Simulando APIs REST Externas: Parte 1 (MockK, Mockito e Stubs Clássicos)
Depois de concluirmos esta aula bônus, nós:
- Saberemos como criar mocks e stubs de componentes que consomem APIs REST externas.
- Saberemos registrar e utilizar esses test doubles dentro do
ApplicationContextdos nossos testes de integração. - Entenderemos os prós e contras de utilizar uma biblioteca de mocking (MockK / Mockito) para substituir o serviço cliente.
- Identificaremos os prós e contras de implementar Stubs Clássicos (classes concretas de teste).
- Saberemos escolher a melhor ferramenta para cada cenário.
1. Introdução ao Sistema Sob Teste (SUT)
Nosso sistema sob teste consiste em quatro componentes que trabalham juntos para consultar a alíquota de IVA (VAT percentage) de um país:
1. O Controller VatPercentageController
A classe VatPercentageController fornece um endpoint REST simples utilizado para consultar a porcentagem de imposto (VAT) aplicada em um país específico.
Quando queremos obter a alíquota de um país, enviamos uma requisição GET para o caminho /api/vat-percentage informando o código do país no parâmetro de query countryCode (ex: /api/vat-percentage?countryCode=FI). O controller delega a busca ao método findByCountryCode() da classe VatPercentageService e retorna o objeto VatPercentage encontrado:
|
|
2. O Cliente HTTP VatPercentageService
Como as alíquotas fiscais de diferentes países podem mudar periodicamente, decidimos não manter uma tabela própria de alíquotas e, em vez disso, consultar uma API REST externa. A classe VatPercentageService busca a alíquota em uma API externa utilizando RestTemplate (ou RestClient no Spring Boot 4):
|
|
Nota: Este exemplo didático utiliza uma URL fixa (
http://localhost:8080/api/external/vat-percentage) porque ela atende aos requisitos desta aula e da próxima aula com WireMock. Em aplicações reais de produção, sempre injete a URL base via@ConfigurationPropertiesou variáveis de ambiente!
3. O DTO VatPercentage
A classe VatPercentage é um simples Data Transfer Object (DTO) que armazena o código do país (countryCode) e sua respectiva alíquota (vatPercentage):
|
|
4. Isolando o Bean Real com @Profile na Configuração da Aplicação
Não queremos invocar uma API externa real na internet quando executamos nossos testes de integração, pois isso criaria um acoplamento indesejado e perigoso:
- Nossos testes de integração falhariam sempre que a API externa estivesse fora do ar ou instável.
- A latência de rede deixaria nossa suíte de testes de integração muito mais lenta.
Por isso, garantimos que o bean “real” de VatPercentageService seja registrado no container do Spring apenas quando a aplicação real estiver rodando (ou seja, quando o perfil Profiles.APPLICATION estiver ativo), utilizando a anotação @Profile:
Dica de Arquitetura: Embora possamos colocar
@Profilediretamente em uma classe anotada com@Service/@Component, é muito mais fácil localizar todos os beans dependentes de perfil quando os declaramos explicitamente via métodos@Beanem nossa classe de configuração!
Veja como fica nossa classe principal ExampleApplication:
|
|
A seguir, veremos três maneiras diferentes de fornecer um bean VatPercentageService para os nossos testes de integração (quando o perfil Profiles.INTEGRATION_TEST estiver ativo) sem depender da API externa. Começaremos criando um Stub com MockK / Mockito!
2. Criando um Stub do Nosso Serviço com MockK / Mockito
Se quisermos criar um stub que seja gerenciado pelo container do Spring, criamos uma classe de configuração de contexto (IntegrationTestStubContext) que importa a configuração de ExampleApplication e registra um bean VatPercentageService para o perfil Profiles.INTEGRATION_TEST:
|
|
Por que não configuramos o retorno do stub dentro de
IntegrationTestStubContext? Porque a mesma classe de configuração de contexto costuma ser compartilhada por diferentes testes de integração que precisam que o método do stub retorne valores diferentes (ou lance exceções diferentes) em cada cenário!
Depois de criarmos o bean stub, podemos escrever nossos testes de integração seguindo estes 5 passos:
Passo 1: Criar a Classe de Teste e Configurar o MockMvc
Criamos a classe FindVatPercentageStubTest, configuramos @SpringBootTest(classes = [IntegrationTestStubContext::class]), @AutoConfigureMockMvc e ativamos o perfil @ActiveProfiles(Profiles.INTEGRATION_TEST).
Passo 2: Declarar as Constantes e Injetar o Stub VatPercentageService
Adicionamos as constantes COUNTRY_CODE = "FI" e VAT_PERCENTAGE = 24, e injetamos tanto o MockMvc quanto o bean VatPercentageService via @Autowired.
Passo 3: Implementar o Método @BeforeEach (Resetar e Configurar o Stub)
No método @BeforeEach, executamos duas etapas fundamentais:
- Resetamos o estado do nosso stub (
clearMocks(service)no MockK ouMockito.reset(service)no Mockito). Precisamos fazer isso porque o nosso stub é um bean singleton gerenciado pelo container do Spring e reutilizado entre todos os testes! - Configuramos o objeto
VatPercentageretornado quandoservice.findByCountryCode(COUNTRY_CODE)for chamado (every { service.findByCountryCode(COUNTRY_CODE) } returns foundougiven(service.findByCountryCode(COUNTRY_CODE)).willReturn(found)).
Passo 4: Criar o Método Auxiliar de Requisição (findByCountryCode)
Criamos um método privado findByCountryCode(countryCode: String): ResultActions que envia a requisição GET /api/vat-percentage?countryCode=... utilizando o MockMvc.
Passo 5: Escrever os Três Testes de Integração
Escrevemos três testes de integração verificando:
- O código de status HTTP
200 OK. - O
Content-TypeJSON (application/json). - O corpo JSON retornado (
$.countryCodeigual a"FI"e$.vatPercentageigual a24).
Veja o código completo da classe FindVatPercentageStubTest:
|
|
Vamos descobrir agora como usar um Mock quando queremos verificar a interação entre o controller e o serviço.
3. Criando um Mock do Nosso Serviço com MockK / Mockito
Se quisermos verificar que o sistema sob teste (VatPercentageController) realmente invoca um método do nosso test double com o parâmetro esperado, devemos utilizar um Mock em vez de um Stub.
Para deixar nossa intenção cristalina para quem lê o código de teste, criamos a classe de configuração IntegrationTestMockContext:
|
|
Na classe de teste FindVatPercentageMockTest, fazemos três ajustes em relação à classe anterior:
- Apontamos
@SpringBootTestparaIntegrationTestMockContext::class. - Como neste teste queremos apenas verificar a interação (e não o corpo da resposta), usamos um mock
relaxed = true(que retorna um valor padrão, exatamente comoMockito.mock()) e limpamos o estado do mock no@BeforeEach. - Escrevemos o teste que verifica que
service.findByCountryCode(COUNTRY_CODE)foi invocado exatamente uma vez (verify(exactly = 1)no MockK ouverify(service, times(1))no Mockito):
|
|
Vamos ver agora a terceira alternativa: como criar um Stub Clássico escrito à mão!
4. Criando um Stub Clássico (VatPercentageServiceStub)
Se quisermos implementar um stub clássico (uma subclasse ou implementação de interface fixa, sem usar bibliotecas de mocking) e utilizá-lo nos nossos testes de integração, seguimos estes três passos:
Passo 1: Implementar a Classe VatPercentageServiceStub
- Criamos uma classe
VatPercentageServiceStubque estendeVatPercentageService. - Passamos
nullpara o construtor da superclasse (super(null)), já que o stub não fará chamadas HTTP. - Sobrescrevemos o método
findByCountryCode()retornando uma alíquota constante (24) para qualquer código de país informado:
|
|
Passo 2: Criar a Classe de Configuração IntegrationTestClassicStubContext
Criamos uma classe de configuração que registra uma instância de VatPercentageServiceStub quando o perfil Profiles.INTEGRATION_TEST estiver ativo:
|
|
Passo 3: Escrever a Classe de Teste FindVatPercentageClassicStubTest
Veja a grande vantagem de usar um Stub Clássico: nossa classe de teste de integração não precisa sequer saber que o stub existe! Não precisamos injetar VatPercentageService, não precisamos de @BeforeEach para resetar estado e não acoplamos o teste à assinatura interna do serviço:
|
|
5. Qual Técnica Devemos Utilizar? (MockK/Mockito vs Stub Clássico)
Ambas as técnicas têm prós e contras:
- MockK / Mockito oferece suporte extremamente versátil tanto para stubbing quanto para mocking, dando controle total sobre o test double em cada teste. A desvantagem é que precisamos resetar o test double no
@BeforeEache configurar seus retornos dentro da classe de teste. - Stubs Clássicos são completamente invisíveis para as classes de teste de integração, tornando os testes mais simples e fáceis de manter. A desvantagem é que eles não são práticos quando precisamos retornar valores diferentes por argumento ou simular exceções específicas em cada método de teste.
Podemos escolher a técnica ideal seguindo estas três regras:
- Se precisarmos verificar as interações que aconteceram entre o sistema sob teste e o nosso test double, devemos criar um Mock com MockK / Mockito (não devemos reinventar a roda).
- Se o nosso stub precisar retornar valores diferentes em diferentes testes (ou se quisermos destacar explicitamente o valor retornado dentro do código do teste), devemos criar um Stub com MockK / Mockito.
- Se o nosso stub puder retornar sempre o mesmo valor constante, devemos escrever um Stub Clássico, pois ele mantém nossos testes de integração muito mais limpos e fáceis de manter.
Resumo
Nesta aula, aprendemos que:
- Substituir o bean
VatPercentageServicepor um test double (com MockK/Mockito ou Stub Clássico) é simples e rápido. - Entretanto, a grande desvantagem dessa abordagem é que nossos testes de integração ignoram completamente o código dentro de
VatPercentageServiceque faz a chamada HTTP e deserializa a resposta da API externa! - Na próxima aula (Parte 2), resolveremos exatamente esse problema utilizando o WireMock para simular o servidor HTTP da API externa!