Artigo original: How to Perform Integration Testing using JUnit 5 and TestContainers with SpringBoot
Escrito por: Sameer Shukla
TestContainers é uma biblioteca que ajuda você a executar contêineres Docker específicos do módulo para simplificar o teste de integração.
Esses contêineres do Docker são leves e, uma vez que os testes são concluídos, os contêineres são destruídos.
Neste artigo, vamos entender o que é o TestContainers e como ele ajuda você a escrever testes mais confiáveis.
Também vamos entender os componentes importantes (anotações e métodos) da biblioteca que ajudam você a escrever os testes.
Finalmente, aprenderemos a escrever um teste de integração adequado no SpringBoot usando a biblioteca TestContainers e seus componentes.
Limitações do teste com um banco de dados H2 na memória
A abordagem mais comum para o teste de integração hoje é usar um banco de dados H2 na memória. Existem, no entanto, certas limitações para esse método.
Em primeiro lugar, digamos que estamos usando a versão 8.0 do MySQL em produção, mas que nossos testes de integração estão usando H2. Nunca poderemos executar nossos testes para a versão do banco de dados em execução em produção.
Em segundo lugar, os casos de teste são menos confiáveis porque, em produção, estamos usando um banco de dados totalmente diferente – e os testes estão apontando para o H2. A aplicação pode ter problemas em produção, mas os testes de integração podem ser bem-sucedidos.
Eu estava tentando acessar meu serviço RESTful localmente e me deparei com este erro:
"Caused by: org.postgresql.util.PSQLException: FATAL: database "example_db" does not exist".
Ele aconteceu por causa de um problema de permissão, mas os testes locais funcionaram bem.
Por fim, conforme documentado aqui, na seção Compatibility (documentação em inglês), o H2 é compatível com outros bancos de dados apenas até certo ponto. Existem algumas áreas onde o H2 é incompatível. Se você precisar usar "nativeQueries" em uma aplicação do SpringBoot, por exemplo, usar o H2 pode causar problemas.
Apresentando a biblioteca TestContainers
Ao usar TestContainers, podemos superar as limitações do H2.
- Os testes de integração apontarão para a mesma versão do banco de dados que está em produção. Assim, podemos vincular nossa imagem de banco de dados do TestContainer à mesma versão em execução na produção.
- Os testes de integração são muito mais confiáveis porque a aplicação e os testes estarão usando o mesmo tipo e versão de banco de dados, não havendo problemas de compatibilidade, portanto, nos casos de teste.
O que são os TestContainers?
A biblioteca TestContainers é uma API wrapper sobre o Docker. Quando escrevemos código para criar um contêiner nos bastidores, ele pode ser traduzido para algum comando do Docker como, por exemplo:
Este código pode ser traduzido para algo como o seguinte:
docker run -d --env MYSQL_DATABASE=example_db --env MYSQL_USER=test --env MYSQL_PASSWORD=test ‘mysql:latest’
O TestContainers tem um nome de método "withCommand". Você o usa para definir o comando que deve ser executado dentro do contêiner do Docker, o que confirma que o TestContainers é uma API wrapper sobre o Docker.
O TestContainers baixa as imagens MySQL, Postgres, Kafka, Redis e as executa em um contêiner. O MySQLContainer executará um banco de dados do MySQL em um contêiner e os casos de teste podem se conectar a ele na máquina local. Uma vez que a execução termina, o banco de dados desaparecerá – ele simplesmente é excluído da máquina. Nos casos de teste, podemos iniciar quantas imagens de contêiner quisermos.
O TestContainers suporta JUnit 4, JUnit 5 e Spock. Se você for ao site TestContainers.org, basta visitar a seção QuickStart que explica como usá-lo:
O TestContainers tem suporte a quase todos os bancos de dados, do MySQL e Postgres ao CockroachDB. Você pode encontrar mais informações sobre isso no site TestContainers.org, na seção Modules:
O TestContainers também suporta módulos de nuvem como o módulo da GCloud e o módulo do Azure. Se sua aplicação estiver sendo executada no Google Cloud, o TestContainers terá suporte para Cloud Spanner, Firestore, Datastore e assim por diante.
Até agora, no artigo, falamos apenas sobre bancos de dados, mas o TestContainers suporta vários outros componentes, como Kafka, SOLR, Redis e muito mais.
Como usar a biblioteca TestContainers
Neste artigo, vamos explorar o TestContainers com o JUnit 5. Para implementar o TestContainers, precisamos entender algumas anotações, métodos e bibliotecas importantes do TestContainers que precisamos implementar em nosso projeto.
Anotações em TestContainers
Duas anotações importantes são necessárias em nossos testes para que o TestContainers funcione: @TestContainers e @Container.
@TestContainer é uma extensão do JUnit-Jupiter que inicia e interrompe automaticamente os contêineres usados nos testes. Essa anotação encontra os campos que são marcados com @Container e chama os métodos específicos do ciclo de vida do contêiner. Aqui, os métodos do ciclo de vida do MySQLContainer serão invocados.
O MySQLContainer é declarado como estático, pois, se declararmos o contêiner como estático, um único contêiner será iniciado e será compartilhado entre todos os métodos de teste.
Se for uma variável de instância, um contêiner será criado para cada método de teste.
Métodos da biblioteca TestContainers
Existem alguns métodos importantes na biblioteca TestContainers que você usará nos testes. É bom conhecê-los antes de usar a biblioteca.
- withInitScript: usando 'withInitScript', podemos executar o .SQL para definir o esquema, tabelas e também adicionar os dados ao banco de dados. Resumindo, esse método é usado para executar o .SQL para popular o banco de dados.
- withReuse (true): usando o método 'withReuse', podemos habilitar a reutilização de contêineres. Esse método funciona bem em conjunto com a habilitação da propriedade "testcontainers.reuse.enable:true" no arquivo ".testcontainers.properties".
- start: usamos isso para iniciar o contêiner.
- withClasspathResourceMapping: usamos para mapear um recurso (arquivo ou diretório) no classpath para um caminho dentro do contêiner. Isso só funcionará se você estiver executando seus testes fora de um contêiner do Docker.
- withCommand: define o comando que deve ser executado dentro do contêiner do Docker.
- withExposedPorts: usado para definir a porta na qual o contêiner escuta.
- withFileSystemBind: usado para mapear um arquivo/diretório do sistema de arquivos local para o contêiner.
Caso de uso do TestContainers
No exemplo que veremos agora, a aplicação se comunicará apenas com o banco de dados e escreverá os testes de integração para ele usando o TestContainers. Em seguida, estenderemos o caso de uso implementando o Redis no meio.
Se os dados existirem no cache do Redis, eles serão retornados; caso contrário, ele mergulhará no banco de dados para salvar e recuperar com base na chave.
O serviço é simples. Ele tem 2 endpoints – o primeiro é para criar um usuário e o segundo é para encontrar um usuário por e-mail. Se o usuário for encontrado, ele será retornado; caso contrário, obteremos um 404. O código da classe de serviço tem esta aparência:
Vamos escrever os testes para essa classe. Você pode encontrar toda a base de código aqui:
A classe de teste é marcada com a anotação @TestContainers, que inicia/para o contêiner. Usamos a anotação @Container para chamar os métodos específicos do ciclo de vida do contêiner.
Além disso, o "MySQLContainer" é declarado como estático porque, então, um único contêiner é iniciado. Em seguida, ele é compartilhado entre todos os métodos de teste (já discutimos a importância dessas anotações).
Em seguida, precisamos escrever um método de configuração marcado com @BeforeAll, onde habilitamos o método "withReuse". Isso nos ajuda a reutilizar os contêineres existentes. Estamos usando o método "withInitScript" para executar o arquivo ".sql" e, em seguida, iniciar o contêiner.
@DynamicPropertySource nos ajuda a sobrescrever as propriedades declaradas no arquivo properties. Escrevemos esse método para permitir que o TestContainers crie o URL, o nome de usuário e a senha por conta própria – caso contrário, podemos enfrentar erros.
Por exemplo, ao remover o nome de usuário e a senha, podemos enfrentar um erro 'Access denied' (acesso negado, em português), que pode nos confundir. Portanto, é melhor permitir que o TestContainer atribua essas propriedades dinamicamente por conta própria.
É isso – estamos prontos para executar os casos de teste:
Execute @AfterAll para parar o contêiner. Caso contrário, ele poderá continuar sendo executado em sua máquina local se você não o parar explicitamente.
Como usar o GenericContainer
GenericContainer é o contêiner mais flexível. Ele facilita a execução de quaisquer imagens de contêiner dentro do GenericContainer.
Agora que temos o Redis em seu devido lugar, tudo o que precisamos fazer em nosso caso de teste é ativar um GenericContainer com a imagem do Redis.
Em seguida, iniciamos o contêiner genérico do Redis em @BeforeAll e o paramos com o método de desmontagem @AfterAll.
Conclusão
É extremamente fácil usar o TestContainers em nossa aplicação para escrever testes melhores. A curva de aprendizado não é muito íngreme e tem suporte para vários módulos diferentes de uma variedade de bancos de dados, como Kafka, Redis e outros.
Escrever testes usando o TestContainers torna nossos testes muito mais confiáveis. O único lado negativo é que os testes são lentos em comparação aos do H2. Isso ocorre porque o H2 está na memória e o TestContainers leva tempo para baixar a imagem, executar o contêiner e executar toda a configuração que discutimos neste artigo.