Back-End

13 nov, 2013

Introdução ao framework de teste do Spring MVC – Parte 02

Publicidade

O primeiro artigo (LINKAR) desta minissérie introduziu o framework de teste do Spring MVC e demonstrou seu uso em testes unitários com classes controller do Spring MVC em vez de POJOs. Agora é hora de falar sobre utilizar o framework para testes de integração.

Com “testes de integração”, quero dizer carregar arquivos de contexto do Spring no ambiente de teste para que o controller possa trabalhar com seus colaboradores para testes “do início ao fim”.

Mais uma vez, vou escrever um teste para o FacebookPostsController do meu projeto Spring Social Facebook, e o teste será, conforme se espera, uma versão de um teste de integração da minha classe FacebookPostsControllerTest. Se precisar ver o código original do FacebookPostsController ou o código original do FacebookPostsControllerTestecode, veja o meu último artigo. Para uma visão completa do código do FacebookPostsController, veja o artigo sobre o Spring Social Facebook.

O primeiro passo para criar um teste de integração é carregar o contexto do Spring no ambiente de testes. Isso é feito ao acrescentar as seguintes notações à classe FacebookPostsControllerTest:

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @WebAppConfiguration
  3. @ContextConfiguration(“file-names”)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml",
    "file:src/main/webapp/WEB-INF/spring/data.xml" })
public class FacebookPostsControllerTest {

Não há nada de novo no @RunWith(SpringJUnit4ClassRunner.class) ou no @ContextConfiguration(“file-names”), já que eles existem desde o Spring 2.5 e, caso você seja um desenvolvedor Spring, então provavelmente já os utilizou em testes de integração anteriormente. O “novato” é o @WebAppConfiguration.

Essas notações trabalham juntas para configurar o ambiente de testes. @RunWith diz ao JUnit para executar o teste usando a classe de execução do Spring JUnit. @WebAppConfiguration diz ao SpringJUnit4ClassRunner para que o ApplicationContext carregue os testes de integração que devem ser um WebApplicationContex, enquanto que @ContextConfiguration é usado para especificar qual arquivo XML é carregado e de onde.

Nesse caso, estou carregando os arquivos “servlet-context-xml” e “data.xml” do projeto. O arquivo “servlet-context.xml” contém todos os bits e pedaços que você esperaria de um aplicativo web em Spring, tal qual <annotation-driven /> e solucionadores de views, enquanto que “data-xml” contém a configuração da base de dados usada pelos componentes da aplicação do Spring Social. O ponto a se perceber aqui é que estou propositalmente usando arquivos de configuração de pseudo-produção, já que quero rodar um teste de integração do começo ao fim acessando sistemas de arquivo, base de dados etc.

Isso é apenas código de exemplo, e você normalmente não mexeria em base de dados em produção ou em outros recursos relacionados ao seu teste de integração. Você normalmente configuraria seu aplicativo para acessar a base de dados do teste de integração e outros recursos. Uma forma de solucionar esse problema é criar um arquivo XML de configuração para cada módulo Maven em seu projeto; entretanto, não crie, como vi em um projeto, um arquivo XML separado para cada módulo Maven no seu projeto; a razão de ser disso é que, quando você faz uma mudança em seu código, você termina alterando um monte de arquivos de configuração de forma a fazer com que os testes de integração funcionem novamente, o que, além de ser chato, consome um tempo precioso. Uma abordagem melhor é ter uma única versão do seu XML config e usar perfis do Spring para configurar o seu aplicativo para diferentes ambientes. Se escolher usar perfis, você precisará também acrescentar a notação @ActiveProfiles(“nome-do-profile”) às outras três listadas acima. Contudo, isso está além do escopo deste artigo.

Assumindo que você está usando autowiring e que você configurou o <context:component-scan /> corretamente, então o próximo passo é adicionar a variável de instância abaixo para testar sua classe:

  @Autowired
  private WebApplicationContext wac;

Isso diz ao Spring para injetar o WebApplicationContext que foi criado antes para dentro do seu teste. Ele é então usado de maneira muito simples com um método setup() de uma única linha:

  @Before
  public void setUp() throws Exception {

    mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
  }

Como na versão “standalone/programática” desse teste, o objetivo do método setup() é criar uma instância mockMvc e então usá-la para realizar os testes. A diferença é que ela é criada simplesmente usando o WebApplicationContext como um argumento para o MockMvcBuilders.

Depois de ter organizado o método setup(), o próximo passo é escrever um teste e vou reescrever o testShowPostsForUser_user_is_not_signed_in() do meu último artigo, só que como um teste de integração. A surpresa aqui é que o código é muito mais simples do que as versões anteriores do JUnit:

  @Test
  public void testShowPostsForUser_user_is_not_signed_in() throws Exception {

    ResultActions resultActions = mockMvc.perform(get("/posts").accept(MediaType.ALL));
    resultActions.andExpect(status().isOk());
    resultActions.andExpect(view().name("signin"));
  }

Se você comparar esse código com o do testShowPostsForUser_user_is_not_signed_in() do meu artigo anterior, você verá que é quase idêntico. A única diferença é que não há necessidade de configurar nenhum objeto mock.

Neste momento, eu demonstraria um teste de integração do meu teste testShowPostsForUser_user_is_signed_in, mas acontece que isso é um pouco complicado. A razão para tal é que, para conseguir uma lista com os post do Facebook dele, o usuário precisa entrar em sua conta da rede social, e isso significa que há a necessidade de efetuar diversas requisições ao servidor antes de o objeto necessário HttpServletRequest estar no estado correto para facilitar uma chamada ao Facebook para só então recuperar a lista de posts. Isso me pareceu muito complicado para um código de exemplo, e é algo que eu não gostaria de fazer em um projeto real.

Em vez de enxergar toda essa complexidade como uma limitação do framework de teste do Spring MVC, eu diria que isso destaca as melhores práticas, que são para assegurar que as requisições ao seu servidor sejam independentes e atômicas tanto quanto possíveis.

É claro que eu poderia usar objetos mock ou criar um serviço Facebook simulado, mas, novamente, isso está além do escopo deste artigo.

Um bom exemplo de uma requisição atômica e independente para o servidor é a requisição REST testConfirmPurchase_selection_1_returns_a_hat(…) para a classe OrderController tirada do artigo Spring MVC, Ajax e JSON – Parte 02: o código do lado do servidor. Esse código, que está explicado no artigo do Ajax, requisita uma confirmação de compra, que é retornada em JSON.

O código OrderController que retorna JSON é listado abaixo

   /**
   * Create an order form for user confirmation
   */
  @RequestMapping(value = "/confirm", method = RequestMethod.POST)
  public @ResponseBody
  OrderForm confirmPurchases(@ModelAttribute("userSelections") UserSelections userSelections) {

    logger.debug("Confirming purchases...");
    OrderForm orderForm = createOrderForm(userSelections.getSelection());
    return orderForm;
  }

  private OrderForm createOrderForm(List<String> selections) {

    List<Item> items = findItemsInCatalogue(selections);
    String purchaseId = getPurchaseId();

    OrderForm orderForm = new OrderForm(items, purchaseId);
    return orderForm;
  }

  private List<Item> findItemsInCatalogue(List<String> selections) {

    List<Item> items = new ArrayList<Item>();
    for (String selection : selections) {
      Item item = catalogue.findItem(Integer.valueOf(selection));
      items.add(item);
    }
    return items;
  }

  private String getPurchaseId() {
    return UUID.randomUUID().toString();
  }

Enquanto que o JSON que ele retorna se parece com isto:

   {"items":[{"id":1,"description":"description","name":"name","price":1.00}, 
    {"id":2,"description":"description2","name":"name2","price":2.00}],
    "purchaseId":"aabf118e-abe9-4b59-88d2-0b897796c8c0"}

O código que testa o testConfirmPurchases_selection_1_returns_a_hat(…) é exibido abaixo de forma verbosa.

  @Test
  public void testConfirmPurchases_selection_1_returns_a_hat() throws Exception {

    final String mediaType = "application/json;charset=UTF-8";

    MockHttpServletRequestBuilder postRequest = post("/confirm");
    postRequest = postRequest.param("selection", "1");

    ResultActions resultActions = mockMvc.perform(postRequest);

    resultActions.andDo(print());
    resultActions.andExpect(content().contentType(mediaType));
    resultActions.andExpect(status().isOk());

    // See http://goessner.net/articles/JsonPath/ for more on JSONPath
    ResultMatcher pathMatcher = jsonPath("$items[0].description").value("A nice hat");
    resultActions.andExpect(pathMatcher);
  }

O código acima não está da forma que o pessoal na Spring prefere que você escreva, entretanto, num formato verboso, é mais fácil se discutir o que está acontecendo. A estrutura desse método é similar ao método testShowPostsForUser_user_is_signed_in(…) discutido na parte 1. O primeiro passo é criar um objeto postRequest do tipo MockHttpServletRequestBuilder(…) usando o método estático  MockMvcRequestBuilders.post(…). A o parâmetro “selection” com um valor “1” é acrescentado aos objetos resultantes.

O objeto postRequest é então passado ao método mockMvc.perform(…), e um objeto ResultActions é retornado.

O objeto ResultActions é então verificado usando o método andExpect(…) para conferir tanto o status HTTP (ok = 200) quanto para conferir se o tipo do conteúdo é “application/json;charset=UTF-8”.

Além disso, acrescentei uma chamada ao método andDo(print()) para exibir o estado dos objetos HttpServletRequest e HttpServletResponse. A saída para essa chamada é exibida abaixo:

  MockHttpServletRequest:
         HTTP Method = POST
         Request URI = /confirm
          Parameters = {selection=[1]}
             Headers = {}

             Handler:
                Type = com.captaindebug.store.OrderController
              Method = public com.captaindebug.store.beans.OrderForm com.captaindebug.store.OrderController.confirmPurchases(com.captaindebug.store.beans.UserSelections)

  Resolved Exception:
                Type = null

        ModelAndView:
           View name = null
                View = null
               Model = null

            FlashMap:

MockHttpServletResponse:
              Status = 200
       Error message = null
             Headers = {Content-Type=[application/json;charset=UTF-8]}
        Content type = application/json;charset=UTF-8
                Body = {"items":[{"id":1,"description":"A nice hat","name":"Hat","price":12.34}],"purchaseId":"d1d0eba6-51fa-415f-ac4e-8fa2eaeaaba9"}
       Forwarded URL = null
      Redirected URL = null
             Cookies = []

Um teste final utiliza o MockMvcResultMatchers.jsonPath(…) para checar que o path do JSON de “$items[0].description” possui um valor de “A nice hat”. Para poder usar o método estático jsonPath(…), você precisa incluir o módulo JSON Path em seu POM.xml para fazer o parser do JSON.

<dependency>
  <groupId>com.jayway.jsonpath</groupId>
  <artifactId>json-path</artifactId>
  <version>0.8.1</version>
  <scope>test</scope>
 </dependency>

JsonPath é uma forma de extrair seletivamente os campos dos dados em JSON. Ele é baseado na ideia do XML XPath.

Obviamente não há necessidade de escrever seus testes na forma verbosa que eu usei acima. O código abaixo mostra o mesmo código da maneira que o pessoal na Spring projetou seu uso:

  @Test
  public void testConfirmPurchases_spring_style() throws Exception {

    mockMvc.perform(post("/confirm").param("selection", "1")).andDo(print())
        .andExpect(content().contentType("application/json;charset=UTF-8"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$items[0].description").value("A nice hat"));
  }

Acho que isso é tudo. Para recapitular, a ideia é acrescentar a notação apropriada a seu teste unitário para que o Spring carregue seu config XML para criar um WebApplicationContext. Isso é então injetado dentro do seu teste e passado ao framework de teste do Spring MVC como um parâmetro ao se criar o mockMvc. Os testes são escritos com uma ideia sendo passada a um objeto MockMvcRequestBuilders construído apropriadamente para o método mockMvc.perform(…), que retorna o valor suposto que é então confirmado para então passar ou falhar em seu teste.

O código para este artigo está disponível no GitHub e nos artigos sobre Facebook e Ajax-JSON.

***

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_17.html#.Uf_G7ZJwrng