DEV Community 👩‍💻👨‍💻

João Victor Martins
João Victor Martins

Posted on • Updated on

[PT-BR] Testcontainers com MySQL e Redis

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.

Image description

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>
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 um Nothing no generics de MySQLContainer 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 é:

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 classe MySQLContainer 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 classe MySQLContainer que recebe o username da database como parâmetro.

  • withPassword: Método da classe MySQLContainer 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 classe GenericContainer 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() } }
    }
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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

Image description

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)

Collapse
 
marcuspaulo profile image
Marcus Paulo

Parabéns pelo excelente artigo João.

Collapse
 
j_a_o_v_c_t_r profile image
João Victor Martins Author

Valeu, Marcus =)

Collapse
 
paulo_iggor profile image
Paulo Igor

Muito bom!

Collapse
 
j_a_o_v_c_t_r profile image
João Victor Martins Author

Valeu, Paulo =)

🌚 Life is too short to browse without dark mode