Separando os testes integrados de sua aplicação em um novo conceito

Guilherme Biff Zarelli
luizalabs
Published in
7 min readJan 13, 2022

O objetivo desse artigo é mostrar como manter seus testes integrados separados da aplicação, com uma separação física, para que haja uma melhor distinção e execução entre testes de unidade e integração.— com exemplos em Java

Inspirado nas arquiteturas como a Hexagonal ou Clean Architecture que tentem a deixar o Framework como um detalhe de implementação, e assim como ele, os testes integrados devem seguir os mesmos princípios.

Antes de iniciarmos o como separamos os testes integrados nesse novo conceito, vamos entender de forma bem sucinta a diferença entre testes integrados (também chamados de testes de serviço) e de unidade:

  • Os testes de integração determinam se as unidades de software desenvolvidas independentemente funcionam corretamente quando estão conectadas umas às outras.
  • Os testes de unidade têm o escopo mais restrito de todos os testes em seu conjunto. Os testes unitários devem garantir que todos os seus caminhos de código não triviais sejam testados, porém eles não devem estar muito ligado à sua implementação.

Há uma grande diferença entre os testes de unidade e de integração. Enquanto o de integração aborda o sistema como uma série de módulos e examina as interações e a conectividade adequada entre eles, o outro testa o produto unidade por unidade sem que haja quaisquer agente externo conectado nela.

Veja mais sobre o assunto e entenda sua diferença e escopo nesse artigo no qual abordo com um pouco mais de detalhes: Pirâmide de Testes — Definindo uma boa suíte de testes para seu Software

Um dos principais motivadores para a separação dos testes integrados da aplicação é sua aplicabilidade. Muitos desenvolvedores não entendem de fato sua diferença e acabam criando uma suíte de testes difícil de se manter, com uma mistura de tipos distintos na mesma execução. O desenvolvimento de novas funcionalidades ou refatorações tendem a ser morosas, pois o processo constante de execução dos testes sempre englobará uma suíte complexa com carregamento de Frameworks e demais dependências.

Quando estamos falando de aplicações Java, o Maven como build tool por exemplo, nos fornece em seu ciclo de execução a distinção do que é test de unidade de teste de integração, no qual, o phaseintegration-test’ está em um nível superior ao do ‘test’ (Veja o lifecycle), porém grande parte dos desenvolvedores acabam não configurando adequadamente o plugin com tags ou nomes dos arquivos para que haja essa distinção em sua execução.

Em outro aspecto, quando estamos falando de aplicações modulares, como a Clean Architecture (ou até mesmo quem queira usar seus conceitos sem a modularização) que tem como premissa tornar o uso de Framework apenas um detalhe de implementação, os testes de integração também se tornará um problema, pois normalmente estamos desenvolvendo em cima de especificações, o que nos faz pensar em qual lugar seria realmente mais adequado inserir os testes de integração do sistema para que fiquem totalmente isentos ao Framework e reutilizáveis caso venhamos a testar outros?

O Acceptance Teste

Como solução, decidimos criar um módulo capaz de encapsular em uma imagem Docker a aplicação e executar os testes de maneira mais fiel ao comportamento real da aplicação. Dessa maneira conseguimos garantir que haja uma homogeneidade no modulo da aplicação, no qual executaremos apenas testes de unidade, e os integrados no Accpetance Test.

Arquitetura de testes de uma aplicação com Acceptance Test

Dependências utilizadas

Para configurarmos o módulo, utilizamos algumas dependências conceituadas no mercado.

  • Test Conteiner: proporciona integração com qualquer imagem Docker, conseguimos configurar um MySQL, Flyway, entre outros serviços necessários para execução de um teste completamente integrado, assim para cada teste integrado, subimos uma instância da aplicação totalmente pronta.
  • REST Assured: Chamadas de APIs, validações de contratos com seu Schema Validator e asserts de respostas dos requests. Dessa maneira, conseguimos ter fluência na escrita dos testes e segurança para validações de contrato e respostas de solicitações.
  • Wire Mock: Utilizado para mocks de quaisquer chamadas a serviços externos de nossa aplicação.

Além das depêndencias apresentadas também utilizamos os seguintes plugins na tag build:

  • Fabric8: Utilizado para construir imagens Docker e gerenciar contêineres para testes de integração. Utilizamo para realizar a construção da imagem do projeto, para ser utilizado na execução dos testes do módulo. (utilizado no phase: pre-integration-test)
  • maven-failsafe-plugin: O plug-in Failsafe foi projetado para executar testes de integração. Com ele garantimos que os testes executados por esse módulo sejam executados no phase integration-test do Maven.

Configuração do módulo

Supondo que seu projeto não seja modular o trabalho será um pouco maior, teoricamente, você teria que criar um pom root e transformar o que é sua base atual em um módulo (‘app’ por exemplo), assim, alterariamos o pom da aplicação para utilizar o pom root como parent — Artigo sobre multi-module com o Maven: https://www.baeldung.com/maven-multi-module.

Considerando que o projeto esteja em uma estrutura modular, basta criarmos um novo módulo e configurar seu respectivo pom.xml.

Segue um exemplo em um projeto já implementado:

Observações e comentários importantes sobre o pom.xml do módulo:

  • O plugin fabric8 é configurado para gerar a imagem Docker a partir do Dockerfile mantido na raiz do projeto, justamente para tentar manter o comportamento real da aplicação, utilizando o mesmo Dockerfile produtivo.
  • Ainda sobre o fabric8, como não estamos gerando uma imagem Docker a partir de seu módulo de origem, devemos adicionar um arquivo .maven-dockerignorena raiz do projeto para ignorar a compilação do target do mesmo, com o seguinte conteúdo:
acceptance-test/target/docker/**
  • Note que o pom foi configurado para ser executado apenas nos steps de integration-test pelo Maven. O plugin do fabric8 está configurado no phase pre-integration-test justamente para a imagem Docker ser gerada antes da execução do plugin maven-failsafe-plugin que é de fato o executor do phase integration-test responsável pela execução dos testes mantidos no módulo.
build configuration in acceptance-test pom.xml

Os testes, código

Para iniciar os testes, temos que primeiramente iniciar os containers, com o TestContainers temos a possibilidade de configurar as imagens necessárias e inicia-las em tempo de execução nos testes.

Criaremos uma classe capaz de configurar nosso ambiente e que seja a base dos testes. Para conseguirmos desenvolver os testes para nossos endpoints precisaremos configurar o ambiente, como: Banco de dados, Aplicação, o Mock Server (Wire Mock) e o Rest Assured:

Adicionamos as inicializações dos containers no bloco static da classe, justamente para cria-los apenas uma vez, reutilizando as dependências entre as execuções dos testes.

A idéia é criarmos nossa aplicação o mais próximo da realidade (produção), o TestContainers nos proporciona todas as ferramentas necessárias para a criação de containers como se tivessemos criando um docker-compose, muito simples e objetivo, veja:

Agora que temos nosso ‘starter’, podemos evoluir para a criação dos testes e usaremos o RestAssured e o MockServer para isso.

No exemplo a seguir, criamos uma classe de teste, que extende nosso ‘starter’, com isso, sabemos que nossos containers e configurações necessárias para execução dos testes estejam ok.

Nossa aplicação se trata de um agendador de mensagens, no qual, ao realizarmos um agendamento ele nos devolve um id do request e um protocolo de acompanhamento que é gerado em um serviço externo.

O teste em si, realiza um mock do serviço externo (linha 5) que é responsável pela geração do protocolo da mensagem, depois criamos um agendamento (linha 9–18) e utilizamos seu ‘id’ para consultar e valida-lo. (linha 20–28).

Simples não? dessa maneira conseguimos realizar os testes integrados da aplicação muito mais fiel ao ambiente produtivo e desacoplado ao framework, tornando-o mais durável a possíveis mudanças.

Garantindo, que tenhamos o Lifecycle do Maven mais fiel, dessa forma conseguimos executar de forma distintas seus phases:

  • $mvn compile -> Realiza a compilação do projeto sem a execução de quaisquer testes.
  • $mvn test -> Realiza apenas a execução dos testes unitários do projeto
  • $mvn verify -> Realiza a execução de todos os testes unitários e os integrados contidos no novo módulo.

Implementamos esse módulo no projeto a seguir, veja em mais detalhes todos os pontos que mencionamos:

Conclusão

Com uma separação física, temos uma melhor distinção e execução dos testes de unidade e integração, ajudando os time a entenderem a diferença entre eles ganhamos em qualidade, com testes mensurados de formas distintas, evitamos que testes integrados em um mesmo módulo interfiram na cobertura dos testes unitários, já a execução de forma distinta, nos da agilidade no desenvolvimento e refatorações.

A idéia do módulo nos permite até mesmo criarmos um similar para testes E2E, ou até mesmo no mesmo (tomando cuidado sempre com a granularidade), basta configurarmos nosso Test Conteiner com environments de ambientes de integração.

A abordagem apresentada, pode ser útil em qualquer projeto, o Test Conteiner nos abre diversas possibilidades que nos permite encaixar diferentes tipos de soluções.

Referências

--

--