[PT-BR] Testcontainers com MySQL e Redis
Tags: testing, ptbr, kotlin, braziliandevs
Published: 2022-06-16 12:51:51

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hrdgcdu7bvfbk9tvhdqn.jpeg) 

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. 

```xml
# 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

```java
// 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 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. 

```java
@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. 

```sql
# 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. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4l2mcnuoc3noogwo9s1j.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c9fb7e5y69a8a1tpgpxs.png)

#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