Testes integrados com Wiremock

Marcos Faneli - Aug 27 - - Dev Community

A um tempo atrás de escrevi um artigo sobre teste de integração para APIs com spring (https://dev.to/marcosfaneli/testes-integrados-com-spring-4n5d). Neste artigo a ideia era apresentar de maneira simples como começar construir uma aplicação com cobertura de teste desde seu inicio, mas convenhamos nenhuma (ou quase nenhuma) na vida real vai se apresentar assim como o exemplo, um CRUD simples e mais nada.
Este pensamento me levou a pensar em evoluir esse projeto e acrescentar a ele algumas situações de vida real.

Para começar vamos olhar para o projeto inicial:
https://github.com/marcosfaneli/spring-with-integration-test/tree/feat/refactor.

Este é o mesmo projeto usado no artigo anterior, mas para facilitar a vida fiz alguns refactor, para deixar o código mais coeso e para facilitar especialmente nosso trabalho aqui (e por que eu gosto de refatorar código também).

Qual o desafio?

Recapitulando rapidamente o projeto base, a ideia de "negócio" dele. O projeto se trata de aplicação que irá controlar ordens de serviço. Para tal ela recebe uma payload com a descrição do pedido de serviço e persiste numa base dados, no caso um Postgres.

Payload entrada

{
   "description": "Some description"
}
Enter fullscreen mode Exit fullscreen mode

Saída

{
   "id": "xxxxxxxxxxxxx",
   "description": "Some description",
   "created_at": "2024-02-21T23:58:01:000z",
   "updated_at": "2024-02-21T23:58:01:000z",
   "status": "OPENED"
}
Enter fullscreen mode Exit fullscreen mode

Então qual vai ser o desafio? Vamos fazer uma evolução, na criação dessa ordem de serviço queremos que ter informado quem é o cliente que solicitou a criação dela. Então vamos alterar nosso payload de entrada para:

{
  "customerCode": "123456",
  "description": "Some description"
}
Enter fullscreen mode Exit fullscreen mode

Veja que no payload acima temos um novo campo customerCode, este campo irá trazer o código do cliente solicitante e ai começa a diversão. Como é muito comum em nossos cenários do dia-a-dia, precisaremos obter os dados do cliente em uma outra API que irá nos fornecer os dados dos cliente.
Este é um cenário bastante comum em organizações que implementam arquiteturas de microsserviços, onde os dados de clientes provavelmente estarão em outro serviço e consequentemente (ou na maioria das vezes) em outra base de dados.
Um outro cenário comum é construirmos aplicações que precisaram se conectar a alguma outra aplicação, já existente que nos forneça este dados.
Tendo em vista este cenário nosso analista/PO/qualquer outro papel, nos apresentou que iremos consumir uma API Rest "xpto" que irá nos retornar esses dados de cliente com o seguinte formato:

URL: GET http://xpto/customers/{customerCode}

{
  "id": "123456",
  "name": "John Doe",
  "email": "john@doe.com"
}
Enter fullscreen mode Exit fullscreen mode

Mockando nossa API Externa

Neste exemplo falamos acima que iremos usar uma api externa, onde iremos consultar o dados de cliente. Bem este é um cenário bem comum no dia, outro cenário bem comum é precisarmos consumir uma api que ainda não temos acesso, ou mesmo uma api que ainda não existe. Eu sei, o primeiro desejo é cruzar os bracinhos, bater o pezinho e dizer "Se a api nem existe ainda/não temos acesso, por que é que estamos nos adiantamos em consumi-la?". A resposta é alguém te um prazo e você é quem vai cumpri-lo, aceite isso e livre de horas de frustração. Para tal situação há várias estratégias válidas, eu irei apresentar aqui uma que tenho usado à algum tempo e que agiliza muito trabalho, trata-se do Wiremock, esta ferramenta oferece uma série de ferramentas que permite lidar com esse problema (Se você é um feliz usuário de Intellij, ele oferece alguns plugins bem legais, na versão paga é claro). Vamos falar sobre dois usos dele, primeiro como mock externo simulando nossa possível api e segundo fazendo o mock nos nossos teste integrados. Vamos começar então fazendo a configuração standalone, que é basicamente subir um serviço HTTP no meu/no seu computador onde faremos que ele retorne um payload específico quando uma chamada for feita para ele, esses retorno são resultados estáticos que iremos gera-los a partir de arquivos JSON onde poderemos definir qualquer estrutura que nos for necessária. (A documentação detalhada pode ser encontrada em https://wiremock.org/docs/standalone/java-jar/).

Baixaremos o jar no link https://wiremock.org/docs/download-and-installation/

Image description

Uma vez baixado, aqui valem duas dicas, se você pretende usa-lo para "mockar"(sim, eu aportuguesei o temos mock) tudo que quiser usar na sua maquina, você pode coloca-lo em uma pasta onde poderá aciona-lo e altera-lo sempre que necessário, particularmente eu uso bastante isso, é bem comum que no dia-a-dia você precise criar diversas integrações com diversas apis e sistemas diversos podem precisar dos mesmos mocks, nesse sentido já cheguei até mesmo colocar uma alias no bash para acionar o Wiremock com apenas uma comando a qualquer momento. Agora uma outra abordagem válida e que pode ajudar a vida do coleguinha que irá mais tarde precisar rodar a sua aplicação é colocar essa configuração a seguir dentro da pasta da sua própria aplicação e documentar muito bem no readme como executar o mock, assim o próximo terá uma vida uma vida um pouco mais fácil.
Vou seguir esse segunda abordagem aqui, sendo assim dentro da pasta raiz do projeto iremos criar uma pasta que chamaremos de mock.

Image description

Pronto, agora coloque o .jar baixado dentro desta pasta. E logo após isso vamos criar dentro desta pasta, mais duas outras pastas.

  • __files
  • mappings

Image description

Vamos criar nosso primeiro mock, dentro da pasta mappings, vamos criar um arquivo customer.json com o seguinte conteúdo:

 {
  "request": {
    "method": "GET",
    "url": "/api/customers/1"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": "{\"id\":\"1\",\"name\":\"John Doe\",\"email\":\"john@doe.com\"}"
  }
}

Enter fullscreen mode Exit fullscreen mode

O arquivo já é bem auto-explicativo, mas acho que valem alguns pontos a serem discutidos. Ele é dividido em request e response, na parte do request você pode definir qual será o método HTTP da chamada, podemos usar qualquer um, e qual será o path que o Wiremock irá disponibilizar o endpoint. No response estão dispostos qual o statusCode que desejamos retornar, headers específicos e o body, note que este é um campo string, assim o JSON precisará ser colocado neste formato.
Pronto, com essa simples estrutura já temos nossa API de customers, para testar basta via terminal(use o de seu gosto), navegar atá a pasta mock que criamos acima e digitar o seguinte commando:

java -jar wiremock-standalone-3.8.0.jar --port 9999

Uma vez executado se formos no navegador poderemos ver que ele esta funcionando:

Image description

Agora sim, ajustando a aplicação?

Vamos incluir a nossa chamada a api na criação de uma nova ordem de serviço.
Então a primeira coisa que precisaremos alterar nosso Dto, adicionando o novo campo customerCode, como falamos acima, assim o record app.controller.dto.CreateOrderDto de verá ficar com a seguinte estrutura:

public record CreateOrderDto(String description, String customerCode) {
}
Enter fullscreen mode Exit fullscreen mode

Com isso podemos alterar o nosso UseCase CreateOrder. Hoje ele te a seguinte estrutura.


import com.seuprojeto.integrationtest.app.controller.dto.CreateOrderDto;
import com.seuprojeto.integrationtest.domain.Order;
import com.seuprojeto.integrationtest.infra.OrderRepository;
import org.springframework.stereotype.Service;

@Service
public class CreateOrder {

    private final OrderRepository repository;

    public CreateOrder(OrderRepository repository) {
        this.repository = repository;
    }

    public Order execute(CreateOrderDto createOrderDto) {
        final Order order = Order.create(createOrderDto.description());
        return this.repository.save(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

Extremamente simples, simplesmente faz o parser e do DTO para a nossa classe de domínio e persiste na banco.
Então vamos implementar aqui a nossa chamada para a API de customer, que acabamos de "mockar" acima. Vamos usar o RestTemplate, se você esta pensando "Mas o RestTemplate está deprecated", você está certo, fique a vontade para trocar esta chamada por qualquer outro componente e notar qu ainda sim ao fim deste artigo os testes irão funcionar. Para fazer a implementação primeiramente vamos declarar o endereço da nossa api na properties, então no arquivo application.yaml vamos adicionar as seguintes linhas:

customer:
  api:
    url: http://localhost:9999/api/customers
Enter fullscreen mode Exit fullscreen mode

Note que ali adicionamos o endereço da nossa api que subimos com Wiremock, outro ponto importante é que precisamos adicionar no application, tanto do código da aplicação, quanto no application dos test.

Para que possamos usar o RestTemplate melhor vamos criar o DTO que irá receber os dados que serão retornados pela nossa API, assim criaremos uma novo record em com/seuprojeto/integrationtest/app/usecase/dto:

package com.seuprojeto.integrationtest.app.usecase.dto;

public record CustomerDto (String id, String name, String email) {
}
Enter fullscreen mode Exit fullscreen mode

Feito isso voltemos ao código do nosso CreateOrder e altera-lo para o seguinte código:

package com.seuprojeto.integrationtest.app.usecase;

import com.seuprojeto.integrationtest.app.controller.dto.CreateOrderDto;
import com.seuprojeto.integrationtest.app.usecase.dto.CustomerDto;
import com.seuprojeto.integrationtest.domain.Order;
import com.seuprojeto.integrationtest.infra.OrderRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.stereotype.Service;

@Service
public class CreateOrder {

    @Value("${customer.api.url}")
    private String customerApiUrl;

    private final OrderRepository repository;

    public CreateOrder(OrderRepository repository) {
        this.repository = repository;
    }

    public Order execute(CreateOrderDto createOrderDto) {

        final String url = customerApiUrl +"/{id}";

        final CustomerDto customer = new RestTemplateBuilder()
                .build()
                .getForObject(url, CustomerDto.class, createOrderDto.customerCode());

        System.out.println("Customer: " + customer);

        final Order order = Order.create(createOrderDto.description());
        return this.repository.save(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

Bem o foco deste artigo é garantir teste, pois bem, se neste momento vc tentar rodar nossos testes irá notar que eles terão quebrado, especialmente onde instanciamos o nosso record CreateOrderDto. Bem você bem lembra que adicionamos a ele um novo campo, o customerCode, resolver isso é fácil, por favor em todas as linhas onde ele tiver quebrado, a usa IDE deve ter acusado, substitua a linha por isto:

Esta:

final CreateOrderDto createOrderDto = new CreateOrderDto(description);
Enter fullscreen mode Exit fullscreen mode

Por esta:

final CreateOrderDto createOrderDto = new CreateOrderDto(description, "1");
Enter fullscreen mode Exit fullscreen mode

Mas vale uma observação, uma alteração quebrou em vários lugares o nosso código, isso pode estar demonstrando um certo acoplamento, o que nos levanta um possível "code smell", para este caso específico considere o uso de Fixture.

Muito bem, feito todos estes ajustes, considerando que seu Wiremock standalone esteja rodando, se executarmos agora nossos teste eles irão passar sem maiores problemas.

Image description

Ainda teríamos que persistir estes dados obtidos na base de dados, mas foge ao foco, deixarei isso feito no código, junto com todo o restante.

Resolvemos todos os nossos problemas?

Bem até aqui conseguimos implementar nossas chamadas e esta funcionando dignamente, mas considere que é bastante comum que os teste sejam executados por exemplo por automatizações durante o deploy da sua aplicação, ai você já deve ter notado que neste cenário talvez o que acabamos de montar não seja satisfatório, já que nosso mock é na verdade uma aplicação sendo executada na nossa maquina, logo teríamos o famoso "Na minha maquina funciona". Não quer dizer que nosso esforço para criar o serviço Wiremock foi em vão, isso pode ser bastante útil em tempo de desenvolvimento da aplicação.

Precisamos garantir que nossos testes possam ser executados independente de integrações externas, isso permitirá que os teste possam ser utilizados em pipelines de deploy automatizadas por exemplo. Para esta solução iremos continuar utilizando o poder do Wiremock, mas agora a sua lib. Para começar vamos adicionar uma nova dependência no pom.xml.

<dependency>
  <groupId>org.wiremock</groupId>
  <artifactId>wiremock-standalone</artifactId>
  <version>3.9.1</version>
  <scope>test</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Com a dependência do Wiremock adicionadas precisaremos configurar no arquivo OrderControllerIntegrationTest uma instancia do WireMockServer para execução dos nossos teste. Esta instancia do WireMockServer irá responder as chamadas externas do nossos serviços, vamos inserir o seguinte trecho de código:


    private static WireMockServer wireMockServer;

    @BeforeAll
    static void setUpWireMock() {
        wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().port(9999));
        wireMockServer.start();

        final CreateOrderDto createOrderDto = CreateOrderFixture.createOrderDto();
        final String customerCode = createOrderDto.customerCode();

        wireMockServer.stubFor(get(urlEqualTo("/api/customers/" + customerCode))
                .willReturn(aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withBody("{\"id\": \"" + customerCode + "\", \"name\": \"John Doe\"}")));
    }
Enter fullscreen mode Exit fullscreen mode

O importante neste método é a annotation @BeforeAll, ela irá garantir que este método será executado antes de todos os outros, obrigatoriamente este método é static, por que ele precisa esta acessível para toda a execução de teste, o método anotado com beforeAll terá sua execução compartilhada por todas a classes de teste durante a execução deste. Uma alternativa pode ser coloca-lo em um classe separada, especialmente se houverem mais recursos a serem compartilhados.

Neste trecho iremos subir um serviço HTTP que irá responder como se fosse a nossa chamada ao o serviço externo, assim não precisaremos mais do serviço Wiremock standalone para a execução dos teste. Note que a porta informada na linha wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().port(9999));é exatamente a mesma porta configurada no arquivo application.yaml na pasta de tests.

Antes de executarmos novamente os teste e verificarmos que eles terão funcionado é importante pensarmos que o beforeAll criou uma instancia de um serviço Http, então é importante ao fim da execução dos teste finalizarmos isso, por isso vamos incluir mais um trecho de código que irá garantir isso:

    @AfterAll
    static void tearDownWireMock() {
        wireMockServer.stop();
    }
Enter fullscreen mode Exit fullscreen mode

Pronto com este último método implementado podemos executar nossos testes sem problemas, veremos que eles irão passar com sucesso, mesmo sem fazer a chamada para serviços externos.

Pontos importantes aqui não abordamos alterações na persistência de dados, incluindo as novas informações que obtivemos da api, tão pouco também fizemos teste para o comportamento esperado da aplicação caso essa api falhe, podemos abordar isso em um novo artigo(bem presunçoso chamar de artigo, não?).

Toda a implementação pode ser observada no repositório do projeto:

https://github.com/marcosfaneli/spring-with-integration-test/tree/feat/article-02

Obs.: Atenção que essas alterações estão na branch feat/article-02.

. .
Terabox Video Player