No post passado, falei sobre testes de unidade e sobre como eles auxiliam na validação das regras de negócio do sistema. Neste post minha ideia é falar sobre testes de integração, que de acordo com o blog da kenzie (2021), é quando os módulos (unidades de código) são testados em grupo para garantir que não haja nenhuma quebra naquilo que foi feito unitariamente e naquilo que está sendo integrado junto. Para exemplificá-lo, realizaremos um teste de integração em uma aplicação existente, onde utilizaremos a biblioteca TestContainers.
Testcontainers
De acordo com a documentação, Testcontainers é uma biblioteca Java que suporta testes JUnit, fornecendo instâncias leves e descartáveis de bancos de dados comuns, navegadores da Web Selenium ou qualquer outra coisa que possa ser executada em um contêiner do Docker.
Os contêineres de teste facilitam os seguintes tipos de testes:
Testes de integração da camada de acesso a dados: use uma instância conteinerizada de um banco de dados MySQL, PostgreSQL ou Oracle para testar seu código de camada de acesso a dados para compatibilidade completa.
Testes de UI/Aceitação: use navegadores da Web em contêiner, compatíveis com Selenium, para realizar testes automatizados de UI.
Muito mais: Confira os vários módulos contribuídos ou crie suas próprias classes de contêiner personalizadas usando
GenericContainer
como base.
Let's code
A aplicação que será utilizada, possui um domínio Car. Podemos realizar operações de inserção de novos carros, de busca de carros disponíveis e de alteração de carros existentes. O sistema possui uma controller, um service e um repository. Na camada de serviço, no método de listar carros, usamos o Redis para cache de informações e o banco de dados é um MySQL. Segue overview da arquitetura.
Será realizado um teste de controller, onde utilizaremos o MockMVC. O MockMvc simula uma requisição real à aplicação, fazendo exatamente o que ocorre quando o cliente chama um endpoint. Isso implica que no momento da execução dos testes, precisamos ter todos os recursos externos funcionando. É comum projetos utilizarem uma instância de banco de dados real, específica para testes, para realizar testes de integração, porém, como vimos anteriormente, temos uma maneira mais elegante e moderna para fazê-los, que é usando TestContainers. Como no fluxo da controller será chamado o método do service cacheado e o repositório, será preciso subir dois contêineres, sendo um pro MySQL e o outro para o Redis. Vamos iniciar o desenvolvimento do nosso teste.
A primeira ação que precisamos fazer é adicionar as dependências do TestContainers no pom.xml
da aplicação.
# Resto do pom.xml omitido
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
Precisamos da dependência do junit-jupiter
do TestContainers, pois ela nos fornece alguns recursos para maior eficiência dos testes e por estarmos usando o MySQL como banco de dados, precisamos de uma dependência que nos forneça recursos para criar um container MySQL para testes de forma simplificada.
Precisamos criar a classe configuração dos contêineres que serão criados no momento do teste. Ela será chamada de DatabaseContainerConfiguration
e terá o seguinte conteúdo
// Imports omitidos
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
abstract class DatabaseContainerConfiguration {
companion object {
@Container
private val mysqlContainer = MySQLContainer<Nothing>("mysql:latest").apply {
withDatabaseName("testdb")
withUsername("joao")
withPassword("12345")
}
@Container
private val redisContainer = GenericContainer<Nothing>("redis:latest").apply {
withExposedPorts(6379)
}
@JvmStatic
@DynamicPropertySource
fun properties(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", mysqlContainer::getJdbcUrl)
registry.add("spring.datasource.password", mysqlContainer::getPassword)
registry.add("spring.datasource.username", mysqlContainer::getUsername)
registry.add("spring.redis.host", redisContainer::getContainerIpAddress)
registry.add("spring.redis.port", redisContainer::getFirstMappedPort)
}
}
}
Agora vamos detalhar o significado de cada um dos recursos acima:
-
@Container
: é usada em conjunto com a anotação@Testcontainers
para marcar contêineres que devem ser gerenciados pela extensão Testcontainers.
Ainda vamos ver sobre a anotação @TestContainers
-
MySQLContainer<Nothing>(...)
: Classe que da suporte a criação de um contêiner MySQL para teste. Passamos umNothing
no generics deMySQLContainer
e isso ocorre por causa da assinatura da classe.public class MySQLContainer<SELF extends MySQLContainer<SELF>> extends JdbcDatabaseContainer<SELF>
, como podemos ver, há um tipo genérico recursivo, que com o Java, conseguimos ignorar e apenas instanciar a classe sem o generics. Usando Kotlin, não temos essa opção, mas temos duas formas de tratar.- Definir uma subclasse de
MySQLContainer
e passar a classe filha para o generics - Usar o
Nothing
, que pelo Kotlin doc's é:
- Definir uma subclasse de
Nothing has no instances. You can use Nothing to represent "a value that never exists": for example, if a function has the return type of Nothing, it means that it never returns*.
Como podemos ver no código, estamos usando a segunda opção. No construtor da classe, é informada a versão que queremos usar da imagem do MySQL.
withDatabaseName
: Método da classeMySQLContainer
que recebe o nome da database como parâmetro. Essa database será criada em tempo de execução, após o contêiner de teste subir.withUsername
: Método da classeMySQLContainer
que recebe o username da database como parâmetro.withPassword
: Método da classeMySQLContainer
que recebe o password da database como parâmetro.GenericContainer<Nothing>()
: Como o próprio nome já diz, é uma classe que permite criar um contêiner de forma genérica, isso significa que os métodos contidos neles, poderão ser reutilizados para vários recursos, como Redis, Bancos NoSQL, entre outros. Estamos o utilizando, pois o Redis não possui uma classe específica como o MySQL. Como estamos vendo no código, para criar um contêiner genérico, basta inserirmos como parâmetro a imagem do recurso que queremos instanciar.withExposedPorts
: Método da classeGenericContainer
que recebe a porta que vamos disponibilizar do contêiner de teste. Essa porta será utilizada em tempo de execução.
A função properties
serve para adicionar as propriedades do Redis e do MySQL de forma dinâmica à aplicação. Ou seja, antes do TestContainers subir os dois contêineres, a aplicação não vai ter conhecimento deles, logo como realizaríamos os testes de integração? Usando o @DynamicPropertySource
o Kotlin entende que essas propriedades serão setadas de forma dinâmica, em tempo de execução, assim, ao subir o contêiner do MySQL e do Redis, as informações que a aplicação precisa conhecer para alcançá-los serão incluídas.
Agora temos tudo que precisamos para que o TestContainers suba os dois contêineres. Precisamos apenas utilizar a classe onde necessário. Como informado no começo da seção, queremos fazer um teste de API, onde o MockMVC fará uma requisição para a controller e assim seguirá o fluxo até o banco de dados. Então vamos criar esse teste.
@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CarControllerTest : DatabaseContainerConfiguration() {
@Autowired
private lateinit var webApplicationContext: WebApplicationContext
private lateinit var mockMvc: MockMvc
companion object {
private const val URI = "/cars"
}
@BeforeEach
fun setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply<DefaultMockMvcBuilder?>(
SecurityMockMvcConfigurers
.springSecurity()).build()
}
@Test
fun `should return code 200 when call cars`() {
mockMvc.get(URI).andExpect { status { isOk() } }
}
Como falado anteriormente, podemos perceber que temos uma anotação na declaração da classe, que é a @TestContainers
. Essa anotação faz com que em tempo de execução, a biblioteca a identifique e suba os contêineres disponíveis pela a anotação @Container
, que está na nossa classe DatabaseContainerConfiguration
, que por sua vez foi herdada pelo teste. Isto é o suficiente para quando executarmos os testes, os contêineres sejam instanciados.
A intenção não é falar sobre o MockMVC, mas de acordo com a documentação, ele é o ponto de entrada principal para suporte de teste Spring MVC do lado do servidor.
Um último detalhe antes de executar os testes, é que será necessário configurar as migrations, para que no momento que a aplicação subir e alcançar os contêineres, as mesmas sejam aplicadas.
# src/main/resources/db/migration/V01__create_car_table_add_data.sql
create table car(
id bigint not null auto_increment,
name varchar(50) not null,
model varchar(50) not null,
primary key(id)
);
insert into car values('Golf', 'VW');
Chegou o momento de executar os testes e para isso o docker deve estar ativo. Executando o comando mvn -B test
, os testes serão executados. Podemos observar os contêineres subindo no momento da execução dos testes, utilizando o comando docker ps
no terminal ou o docker desktop.
Nos logs da execução dos testes, podemos ver o momento em que sobe o contêiner do MySQL
22:17:53.206 [main] DEBUG 🐳 [mysql:latest] - Trying to create JDBC connection using com.mysql.cj.jdbc.Driver to jdbc:mysql://localhost:52550/testdb?useSSL=false&allowPublicKeyRetrieval=true with properties: {password=12345, user=joao}
22:17:53.582 [main] INFO 🐳 [mysql:latest] - Container is started (JDBC URL: jdbc:mysql://localhost:52550/testdb)
22:17:53.583 [main] INFO 🐳 [mysql:latest] - Container mysql:latest started in PT23.428294S
Assim como o do Redis
22:17:53.583 [main] DEBUG 🐳 [redis:latest] - Starting container: redis:latest
22:17:53.583 [main] DEBUG 🐳 [redis:latest] - Trying to start container: redis:latest (attempt 1/1)
22:17:53.583 [main] DEBUG 🐳 [redis:latest] - Starting container: redis:latest
22:17:53.583 [main] INFO 🐳 [redis:latest] - Creating container for image: redis:latest
E finalmente o resultado esperado
Conclusão
Testes de integração possuem muita importância para aplicação, sendo uma das principais atribuições, validar se a conexão da aplicação com recursos externos estão sendo feitas de maneira bem sucedida. Existem N maneiras de fazê-los, podemos usar, por exemplo, para testes de integração com banco de dados, bancos de dados em memória, instâncias reais e como apresentado no post, TestContainers. O TestContainers, com pequenas configurações, traz grandes vantagens. Contêineres descartáveis, pois só são usados no momento da execução dos testes. Agnóstico ao recurso externo, com a classe GenericContainer
, podemos fazer o teste de integração com qualquer recurso que subimos no docker. Uma excelente ferramenta para testes.
Se você sentiu que faltou alguma coisa importante para explicar, ficou com alguma dúvida ou tem alguma sugestão, não deixe de me procurar. No mais, espero que você tenha uma boa leitura. Até a próxima.
Referências
https://kenzie.com.br/blog/teste-de-integracao/
https://www.testcontainers.org/
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/MockMvc.html
Top comments (4)
Parabéns pelo excelente artigo João.
Valeu, Marcus =)
Muito bom!
Valeu, Paulo =)