Back-End

7 nov, 2013

Introdução ao Framework de Teste do Spring MVC – Part 01

Publicidade

Recentemente incluído no Spring principal, o framework de testes do Spring MVC, que o pessoal na Spring afirma ter um “suporte de primeira classe ao JUnit para testar código Spring MVC tanto no lado do cliente quanto no lado do servidor através de uma API fluente”¹. Neste artigo e no próximo, falarei sobre o framework de teste do Spring MVC e o aplicarei a alguns de meus códigos de exemplo existentes para descobrir se ele faz mesmo aquilo que promete.

A API foi projetada com duas formas de configurar os testes no lado do servidor. Estes são: primeiro, com arquivo de contexto do Spring e, segundo, programaticamente sem um arquivo de contexto. O pessoal da Spring refere-se ao método programático como modo “standalone”.

Configurar testes programaticamente parece ser mais parecido com testes unitários, e é melhor utilizado para fazer testes unitários em uma classe controller de forma isolada de seus colaboradores. Por outro lado, o ato de carregar um arquivo de contexto Spring, é mais característico de teste de integração e é indicado para testes de ponta a ponta.

Se você for como eu, então já está utilizando um framework existente como o Mockito ou o Easymock para testar seus controllers. A abordagem convencional do Mockito/Easymock é instanciar um controller, injetar um mock ou as dependências e então chamar o método em teste percebendo o valor de retorno e verificando as chamadas de métodos mock.

O framework de testes do Spring MVC tem uma abordagem diferente dos frameworks mock, sendo que carrega o ServletDispatcher do Spring para emular a operação de um container web. O controller sob teste é então carregado dentro do contexto Spring e acessado pelo DispatcherServlet da mesma forma que seria feito na “vida real”.

O benefício dessa abordagem é que ela permite que você teste um controller como um controller em vez de testá-lo como um POJO. Isso significa que as anotações de um controller são processadas e levadas em conta, ocorre validação e métodos são chamados na ordem certa.

Você pode gostar ou não desse ponto de vista dependendo da sua visão sobre técnicas de teste. Se você for daqueles que acham que toda classe ou método que você testa deve ser isolado ao enésimo grau e que todo teste deve ser atômico, então talvez isso não seja para você. Se você é mais prático e quiser ver o benefício de testar um controller como… um controller, então esse framework pode te interessar.

Tendo uma abordagem diferente do Mockito e Easymock, o lado negativo é que o código parece diferente dessas tecnologias mais antigas e estabelecidas. Ele se baseia bastante em padrões de construção (builder patterns) para construir combinações, construtores de requisições e manipuladores e, depois que você se acostumar, tudo faz sentido. Suspeito que a motivação para fazer uso dos padrões de construção seja simplificar a configuração do mock HttpServletRequest e a interrogação dos objetos do mock HttpServletResponse, que, por definição, poder ser bem complicado.

Neste artigo, avaliarei a técnica programática/standalone da API do Spring, comparando-a com um teste unitário no Mockito.

Para acelerar as coisas, utilizarei o método Blue Peter de “aqui está um que eu preparei antes” e usarei o FacebookPostsController do meu artigo sobre o Facebook, para o qual escreverei duas classes de testes unitários: a primeira usando o Mockito e a outra usando a API de Testes do Spring MVC.

O código do controller se parece com isto:

@Controller
public class FacebookPostsController {

  private static final Logger logger = LoggerFactory
      .getLogger(FacebookPostsController.class);

  @Autowired
  private SocialContext socialContext;

  @RequestMapping(value = "posts", method = RequestMethod.GET)
  public String showPostsForUser(HttpServletRequest request, HttpServletResponse response,
      Model model) throws Exception {

    String nextView;

    if (socialContext.isSignedIn(request, response)) {

      List<Post> posts = retrievePosts();
      model.addAttribute("posts", posts);
      nextView = "show-posts";
    } else {
      nextView = "signin";
    }

    return nextView;
  }

  private List<Post> retrievePosts() {

    Facebook facebook = socialContext.getFacebook();
    FeedOperations feedOps = facebook.feedOperations();

    List<Post> posts = feedOps.getHomeFeed();
    logger.info("Retrieved " + posts.size()
        + " posts from the Facebook authenticated user");
    return posts;
  }
}

Não vou entrar nos bastidores do código, que está disponível no artigo sobre o Facebook; entretanto, para resumir, o aplicativo de exemplo para o Facebook acessa a conta do Facebook do usuário e exibe seu feed de notícias no aplicativo de exemplo. Para fazer isso, o FacebookPostsController checa a classe SocialContext para verificar se o usuário está logado à sua conta do Facebook. Se o usuário estiver logado, então o controller recupera os posts do usuário e os adiciona ao modelo de exibição. Se ele não estiver logado, ele é redirecionado para sua página de login.

Cada uma das duas classes de teste unitário conterá três métodos públicos: setup(), testShowPostsForUser_user_is_not_signed_intestShowPostsForUser_user_is_signed_in. Examinaremos cada um deles.

Como você pode esperar, os testes ShowPostsForUser_user_is_not_signed_in e testShowPostsForUser_user_is _signed_in são usados para testar os casos onde o usuário está e não está logado em sua conta do Facebook.

O teste Mockito “padrão”

  @Before
  public void setUp() throws Exception {

    MockitoAnnotations.initMocks(this);

    instance = new FacebookPostsController();
    ReflectionTestUtils.setField(instance, "socialContext", socialContext);
  }

A configuração do código é bem objetiva e contém três passos simples:

  1. Inicializar os objetos mock usando MockitoAnnotations.initMocks(this).
  2. Criar uma nova instância do objeto sob teste, FacebookPostsController.
  3. Injetar o mock SocialContext no FacebookPostsController.
  @Test
  public void testShowPostsForUser_user_is_not_signed_in() throws Exception {

    when(socialContext.isSignedIn(request, response)).thenReturn(false);

    String result = instance.showPostsForUser(request, response, model);
    assertEquals("signin", result);
  }

O método testShowPostsForUser_user_is_not_signed_in configura o mock SocialContext para retornar false quando o método isSignedIn() é chamado. Isso significa que tudo que resta a fazer é o método showPostsForUser(…) retornar “signin” direcionando o usuário para a página de login.

  @Test
  public void testShowPostsForUser_user_is_signed_in() throws Exception {

    when(socialContext.isSignedIn(request, response)).thenReturn(true);
    when(socialContext.getFacebook()).thenReturn(facebook);
    when(facebook.feedOperations()).thenReturn(feedOps);

    List<Post> posts = Collections.emptyList();
    when(feedOps.getHomeFeed()).thenReturn(posts);

    String result = instance.showPostsForUser(request, response, model);

    verify(model).addAttribute("posts", posts);

    assertEquals("show-posts", result);
  }

O testShowPostsForUser_user_is_signed_in é um tanto complexo. Depois de configurar o mock SocialContext para retornar true quando o método isSignedIn() for chamado, há também quatro linhas de código extra para assegurar que a lista de posts é retornada do feed mock Facebook e adicionada ao novo mock Model. Depois de configurar showPostsForUser(…), há dois passos adicionais: verificar que o mock Model contém a lista de posts e assegurando que o valor retornado de showPostsForUsers(…) é “show-posts”.

Em seguida, veremos o código do framework de teste do Spring MVC, mas antes precisamos acrescentar as seguintes dependências ao arquivo POM:

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>${org.springframework-version}</version>
   <scope>test</scope>
  </dependency>

O framework de teste Spring MVC

A versão do framework de teste do Spring MVC executa os mesmos dois testes, mas de forma diferente…

  @Before
  public void setUp() throws Exception {

    MockitoAnnotations.initMocks(this);
    FacebookPostsController instance = new FacebookPostsController();
    ReflectionTestUtils.setField(instance, "socialContext", socialContext);

    mockMvc = MockMvcBuilders.standaloneSetup(instance).build();
  }

Se você der uma olhada no código acima, poderá ver que, ao menos no setup(…), ele se parece bastante com o código Mockito acima. Como no teste feito com o Mockito, o primeiro passo é inicializar os objetos mock usando MockitoAnnotations.initMock(this), que é seguido pela criação de uma nova instância do FacebookPostsController, na qual o mock SocialContext é injetado. Entretanto, dessa vez o FacebookPostsController foi relegado ao status de uma variável local, uma vez que todo o objetivo dessa configuração é criar uma instância do Spring MockMvc, que é usado para realizar os testes. O mockMvc é criado ao chamar mockMvBuilders.standaloneSetup(instance).build(), onde instance é o objeto FacebookPostsController que estamos testando.

  @Test
  public void testShowPostsForUser_user_is_not_signed_in() throws Exception {

    HttpServletRequest request = anyObject();
    HttpServletResponse response = anyObject();
    when(socialContext.isSignedIn(request, response)).thenReturn(false);

    MockHttpServletRequestBuilder getRequest = get("/posts").accept(MediaType.ALL);

    ResultActions results = mockMvc.perform(getRequest);

    results.andExpect(status().isOk());
    results.andExpect(view().name("signin"));
  }

Como na versão do Mockito, o método testShowPostsForUser_user_is_not_signed_in configura o mock SocialContext para retornar false quando o método isSignedIn() é chamado. Desta vez, o próximo passo é criar algo chamado MockHttpServletRequestBuilder usando o método estático MockMvcRequestBuilders.get(…) e um padrão Builder. Ele é passado ao método mockMVC.perform(…), no qual ele é usado para criar um objeto MockHttpServletRequest que é usado para definir um ponto inicial para o teste. Nesse teste, tudo que fiz foi passar a url “/posts” e configurar a entrada como “any” (qualquer) para o tipo de mídia. Você pode configurar vários outros atributos de objetos de requisição, usando métodos como contentType(), contextPath(), cookie() etc. Para mais informações, veja o Spring javadoc para MockHttpServletRequest.

O método mockMvc.perform() retorna um objeto ResultAction. Isso parece ser um “wrapper” para o MvcResult atual. O ResultsActions é um conveniente objeto usado para assegurar o resultado do teste, da mesma forma que os métodos assertEquals(…) do JUnit ou verify(…) do Mockito. Nesses casos, estou checando que o resultado do status HTTP é ok (ex:. 200) e que a próxima view será “signin”.

A diferença entre esse teste e a versão apenas do Mockito é que você não está diretamente testando o resultado da chamada do método pela sua instância de teste; você está testando o objeto HttpServletResponse que a chamada para o seu método gera.

  @Test
  public void testShowPostsForUser_user_is_signed_in() throws Exception {

    HttpServletRequest request = anyObject();
    HttpServletResponse response = anyObject();
    when(socialContext.isSignedIn(request, response)).thenReturn(true);
    when(socialContext.getFacebook()).thenReturn(facebook);
    when(facebook.feedOperations()).thenReturn(feedOps);

    List<Post> posts = Collections.emptyList();
    when(feedOps.getHomeFeed()).thenReturn(posts);

    mockMvc.perform(get("/posts").accept(MediaType.ALL)).andExpect(status().isOk())
        .andExpect(model().attribute("posts", posts))
        .andExpect(view().name("show-posts"));
  }

A versão do Spring Test para o método testShowPostsForUser_user_is_signed_in é muito similar à versão do Mockito, de forma que o teste é preparado com o método SocialContext.isSignedIn() configurado para retornar true e feedOps.getHomeFeed() configurado para retornar a lista de posts. Parte desse método do framework de teste do Spring MVC é quase idêntic à versão testShowPostsForUser_user_is_not_signed_in descrita acima, com exceção de que desta vez ele checa o nome da próxima view “show-posts” em vez de “sign-in” através do andExpect(view().name(“show-posts”). O estilo do código usado aqui é um pouco diferente do estilo usado acima, e é o estilo preferido do pessoal na Spring. Você pode encontrar mais exemplos desse estilo no Github se verificar o app Spring MVC Showcase.

Portanto, o que podemos concluir com essa comparação? Bem, para ser sincero, não é uma comparação real – a API do framework de teste do Spring MVC, apesar de partir da técnica padrão do Mockito, escolhe uma abordagem diferente, criando um framework que é projetado para testar somente os controllers do Spring MVC em uma simulação do ambiente de execução de seu ambiente nativo.

Se isso é ou não útil para você, deixo para você decidir. Há vantagens em ter os controllers tratados como controllers e não como POJOs, o que significa que eles são testados mais extensivamente. De um ponto de vista pessoal, gosto da ideia de carregar arquivos de configurações do Spring e usá-lo para testes de integração, que é o assunto do qual falarei no meu próximo artigo.

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.captaindebug.com/2013/07/getting-started-with-springs-mvc-test.html#.UjNW2qzyMYp