Introdução
Testes de integração são essenciais para garantir que os diferentes componentes do nosso sistema funcionem juntos como esperado e continuem funcionando após as mudanças.
Neste post, explicarei como criar containers de banco de dados descartáveis para usar em testes de integração.
Testes de integração e recursos gerenciados
Como explicado neste artigo, em testes de integração, devemos simular dependências não gerenciadas (dependências que são externas ao nosso sistema e não controladas por nós, como APIs), mas testar em relação a dependências gerenciadas reais (dependências que são controladas pelo nosso sistema, como bancos de dados). Isso melhora a confiabilidade dos testes de integração porque a comunicação com essas dependências é uma parte complexa do sistema e pode quebrar com uma atualização de pacote, uma atualização de banco de dados ou até mesmo uma simples alteração em um comando SQL.
O que é Testcontainers?
Testcontainers é uma biblioteca que fornece instâncias leves e descartáveis de bancos de dados, navegadores da web selenium ou qualquer coisa que possa ser executada em um container. Essas instâncias podem ser especialmente úteis para testar aplicativos em relação a dependências reais, como bancos de dados, que podem ser criados e descartados após os testes.
Por que não executar os containers manualmente?
Um dos principais benefícios de usar Testcontainers em vez de executar os containers manualmente é que podemos usar bibliotecas como o AutoFixture para gerar dados para popular o banco de dados em vez de executar scripts para inserir os dados. Isso também ajuda a evitar a colisão entre os dados usados em diferentes testes porque os dados são aleatórios.
Além disso, existem outras vantagens em usar Testcontainers:
- Para executar os testes localmente, você não precisa de nenhuma etapa extra, como executar um comando docker-compose;
- Você não precisa esperar ou implementar uma estratégia de espera para verificar se os containers estão em execução antes de acessá-los. O Testcontainers já implementa essa lógica;
- O Testcontainers tem propriedades para acessar a porta em que o container está em execução, então você não precisa especificar uma porta fixa, evitando conflitos de porta ao executar no pipeline CI/CD ou em outras máquinas;
- O Testcontainers para e exclui os containers automaticamente após a execução, liberando recursos na máquina.
Executando um container descartável com Testcontainers
Para usar o Testcontainers, você precisará ter um tempo de execução de container (Docker, Podman, Rancher, etc) instalado em sua máquina.
Em seguida, você precisa adicionar o pacote NuGet Testcontainers ao seu projeto de teste:
dotnet add package Testcontainers --version 3.1.0
dotnet add package Testcontainers.MySql --version 3.1.0
⚠️ Desde a escrita deste post, o Testcontainers lançou a versão 3.1.0 e fez mudanças significativas. Estou atualizando este post para refletir essas mudanças. Para ver o código original com Testcontainers 2.3.*, procure neste branch do repositório.
Para executar um container, primeiro precisamos usar a classe ContainerBuilder
para construir um DockerContainer
ou uma classe derivada, por exemplo, MySqlBuilder
:
|
|
Em seguida, iniciamos o container e usamos o método GetConnectionString
onde necessário:
|
|
❗
DockerContainer
implementaIAsyncDisposable
e precisa ser descartado após o uso. Podemos usar a sintaxeawait using
ou chamar o métodoDisposeAsync
.
Testcontainers tem classes para muitos bancos de dados diferentes (chamados de módulos), por exemplo:
- Elasticsearch;
- MariaDB;
- Microsoft SQL Server;
- MySQL;
- Redis.
A lista completa pode ser vista aqui.
Mas também podemos criar containers a partir de qualquer imagem, como no exemplo abaixo, onde ele cria uma instância Memcached:
|
|
💡 Neste post, explico como usar Testcontainers com Localstack para criar testes de integração para sistemas que usam serviços AWS.
Mais detalhes na documentação oficial.
Criando testes de integração com Testcontainers
Neste exemplo, estou usando o xUnit e a classe WebApplicationFactory<T>
do ASP.NET Core.
Se você não sabe como usar a classe WebApplicationFactory<T>
, expliquei neste post.
Neste exemplo, tenho um controlador com um método GET
que retorna um item ToDo e um método POST
que adiciona um item ToDo:
|
|
⚠️ A lógica de negócios está no controlador apenas por uma questão de simplicidade. Em um aplicativo do mundo real, a lógica deve estar em outra camada.
Testando o método GET
O teste faz as seguintes ações:
- Cria e inicia um container MySql;
- Cria um Entity Framework DbContext usando a ConnectionString do MySql no container;
- Cria as tabelas do banco de dados usando o Entity Framework; (Também pode ser feito passando um script para o método
ExecScriptAsync
do objetomySqlContainer
); - Cria um objeto aleatório com AutoFixture e o adiciona à tabela do banco de dados;
- Substitui a configuração do nosso aplicativo com a string de conexão do container;
- Cria um
HttpClient
apontando para o nosso aplicativo; - Faz uma solicitação para o endpoint GET passando o Id do objeto aleatório que adicionamos ao banco de dados;
- Valida que o Código de Status é
200
e que o objeto retornado é o mesmo que adicionamos ao banco de dados.
|
|
❗ Esteja ciente de que passo a versão da tag do container no método
WithImage
mesmo ao usar a classe tipadaMySqlBuilder
. Isso é muito importante porque quando não passamos a tag, o tempo de execução do container usará a taglatest
e uma atualização do banco de dados pode quebrar o aplicativo e os testes.
Testando o método POST
Primeiro, vamos migrar o MySqlContainer
e a criação do DbContext
para um Fixture e uma Collection para que possam ser compartilhados entre todos os testes. Isso é recomendado porque, a menos que façamos isso, todos os testes criarão e descartarão o container, tornando nossos testes mais lentos do que o necessário.
|
|
O teste faz as seguintes ações:
- O container MySql e o DbContext são injetados pelo fixture xUnit;
- Cria um objeto aleatório com AutoFixture;
- Substitui a configuração do nosso aplicativo com a string de conexão do container;
- Cria um
HttpClient
apontando para o nosso aplicativo; - Faz uma solicitação POST para o endpoint passando o objeto aleatório criado anteriormente;
- Valida que o Código de Status é
200
e que o cabeçalhoLocation
tem o URL correto para o endpoint GET do objeto criado; - Consulta o banco de dados e valida que o objeto criado é igual ao objeto criado aleatoriamente.
|
|