Contentores de teste - Como tornar os testes mais fáceis?
Bartlomiej Kuczynski
Está à procura de uma forma de fazer testes de uma maneira mais fácil? Nós temos a solução! Consulte o seguinte artigo e saiba como torná-lo possível.
O desenvolvimento moderno de aplicações baseia-se numa regra simples:
Utilizar a composição
Compomos classes, funções e serviços em peças maiores de software. Este último elemento é a base do microsserviços e arquitetura hexagonal. Gostaríamos de utilizar as soluções existentes, integrá-las no nosso software e passar diretamente para o mercado.
Pretende gerir o registo de contas e armazenar os dados dos utilizadores? Pode escolher um dos serviços OAuth. Talvez a sua aplicação ofereça algum tipo de subscrição ou pagamento? Existem muitos serviços que o podem ajudar a lidar com isso. Precisa de algumas análises no seu sítio Web, mas não compreende o RGPD? Sinta-se à vontade e escolha uma das soluções prontas a utilizar.
Algo que torna o desenvolvimento tão fácil do ponto de vista empresarial pode dar-nos uma dor de cabeça - no momento em que precisamos de escrever um simples teste.
O Fantastic Beasts: Filas de espera, bases de dados e como as testar
Os testes unitários são bastante simples. Se apenas seguir as regras, então o seu ambiente de teste e código são saudáveis. Que regras são essas?
Fácil de escrever - um teste unitário deve ser fácil de escrever porque se escrevem muitos deles. Menos esforço significa que mais testes são escritos.
Legível - o código do teste deve ser fácil de ler. O teste é uma história. Descreve o comportamento do software e pode ser utilizado como um atalho para a documentação. Um bom teste unitário ajuda-o a corrigir erros sem ter de depurar o código.
Fiável - o teste só deve falhar se houver um erro no sistema que está a ser testado. É óbvio? Nem sempre. Por vezes, os testes passam se os executarmos um a um, mas falham quando os executamos como um conjunto. Eles passam na sua máquina, mas falham no CI (Funciona no meu computador). Um bom teste unitário tem apenas uma razão para falhar.
Rápido - Os testes devem ser rápidos. A preparação para a execução, o início e a própria execução dos testes devem ser muito rápidos. Caso contrário, os testes serão escritos, mas não executados. Testes lentos significam perda de foco. Espera-se e olha-se para a barra de progresso.
Independente - por último, o teste deve ser independente. Esta regra decorre das anteriores. Só os testes verdadeiramente independentes podem constituir uma unidade. Não interferem uns com os outros, podem ser executados em qualquer ordem e as potenciais falhas não dependem dos resultados de outros testes. Independente também significa não depender de quaisquer recursos externos como bases de dados, serviços de mensagens ou sistema de ficheiros. Se precisar de comunicar com recursos externos, pode utilizar mocks, stubs ou dummies.
Tudo se torna complicado quando queremos escrever alguns testes de integração. Não é mau se quisermos testar alguns serviços em conjunto. Mas quando precisamos de testar serviços que utilizam recursos externos como bases de dados ou serviços de mensagens, então estamos a pedir problemas.
Para efetuar o teste, é necessário instalar...
Há muitos anos, quando queríamos efetuar alguns testes de integração e utilizar, por exemplo, bases de dados, tínhamos duas opções:
Podemos instalar uma base de dados localmente. Configure um esquema e ligue-se a partir do nosso teste;
Podemos ligar-nos a uma instância existente "algures no espaço".
Ambos têm prós e contras. Mas ambas introduzem níveis adicionais de complexidade. Por vezes, tratava-se de uma complexidade técnica decorrente das caraterísticas de determinadas ferramentas, por exemplo, a instalação e gestão de uma base de dados Oracle no seu servidor local. Por vezes, tratava-se de um inconveniente no processo, por exemplo, a necessidade de concordar com o teste equipa sobre a utilização de JMS... de cada vez que quiser executar testes.
Contentores para o salvamento
Nos últimos 10 anos, a ideia de contentorização ganhou reconhecimento na indústria. Assim, uma decisão natural é escolher os contentores como uma solução para o nosso problema de teste de integração. Esta é uma solução simples e limpa. Basta executar a construção do processo e tudo funciona! Não acredita? Veja esta configuração simples de uma compilação maven:
com.dkanejs.maven.plugins
docker-compose-maven-plugin
4.0.0
up
testar-compilar
acima
${projeto.basedir}/docker-compose.yml
verdadeiro
down
pós-teste de integração
descer
${project.basedir}/docker-compose.yml
true
E o docker-compose.yml o ficheiro também parece muito bonito!
O exemplo acima é muito simples. Apenas uma base de dados postgres, pgAdmin e mais nada. Quando se executa
bash
$ mvn clean verify
então o plugin maven inicia os contentores e, após os testes, desliga-os. Os problemas começam quando o projeto cresce e o nosso ficheiro de composição também cresce. Cada vez será necessário iniciar todos os contêineres, e eles permanecerão ativos durante toda a construção. É possível melhorar um pouco a situação alterando a configuração de execução do plugin, mas isso não é suficiente. Na pior das hipóteses, os seus contentores esgotam os recursos do sistema antes do início dos testes!
E este não é o único problema. Não é possível executar um único teste de integração a partir do seu IDE. Antes disso, é necessário iniciar os contentores manualmente. Além disso, a próxima execução do maven irá derrubar esses contentores (dê uma olhada em para baixo execução).
Assim, esta solução é como um grande navio de carga. Se tudo funcionar bem, então está tudo bem. Qualquer comportamento inesperado ou invulgar leva nós a algum tipo de desastre.
Testar contentores - executar contentores a partir de testes
Mas e se pudéssemos executar os nossos contentores a partir de testes? Esta ideia parece boa, e já está a ser implementada. Contentores de testeComo estamos a falar deste projeto, aqui está uma solução para os nossos problemas. Não é ideal, mas ninguém é perfeito.
Este é um Java que suporta testes JUnit e Spock, fornecendo maneiras leves e fáceis de executar o Docker contentor. Vamos dar uma olhadela e escrever algum código!
Pré-requisitos e configuração
Antes de começarmos, precisamos de verificar a nossa configuração. Contentores de ensaio necessidade:
Docker na versão v17.09,
Versão mínima de Java 1.8,
Acesso à rede, especialmente ao docker.hub.
Para mais informações sobre os requisitos de um SO e de um IC específicos, consultar em documentação.
Agora é altura de adicionar algumas linhas ao pom.xml.
org.testcontainers
testcontainers-bom
${testcontaines.version}
pom
importar
org.postgresql
postgresql
tempo de execução
org.testcontainers
postgresql
teste
org.testcontainers
junit-jupiter
teste
Eu uso Contentores de ensaio versão 1.17.3mas não hesite em utilizar a mais recente.
Testes com o contentor Postgres
O primeiro passo é preparar a nossa instância de um contentor. Pode fazê-lo diretamente no teste, mas uma classe independente parece melhor.
public class Postgres13TC extends PostgreSQLContainer {
private static final Postgres13TC TC = new Postgres13TC();
private Postgres13TC() {
super("postgres:13.2");
}
public static Postgres13TC getInstance() {
return TC;
}
@Override
public void start() {
super.start();
System.setProperty("DB_URL", TC.getJdbcUrl());
System.setProperty("DB_USERNAME", TC.getUsername()); System.setProperty("DB_USERNAME", TC.getUsername());
System.setProperty("DB_PASSWORD", TC.getPassword());
}
@Override
public void stop() {
// não faz nada. Esta é uma instância partilhada. Deixe a JVM lidar com esta operação.
}
}
No início dos testes, vamos criar uma instância de Postgres13TC. Esta classe pode tratar as informações sobre o nosso contentor. O mais importante aqui são as strings de conexão com o banco de dados e as credenciais. Agora é hora de escrever um teste muito simples.
Neste caso, utilizo o JUnit 5. Anotação @Testcontainers é uma parte das extensões que controlam os contentores no ambiente de teste. Elas encontram todos os campos com @Contentor e contentores de início e paragem, respetivamente.
Testes com o Spring Boot
Como referi anteriormente, utilizo o Spring Boot no projeto. Neste caso, precisamos de escrever um pouco mais de código. O primeiro passo é criar uma classe de configuração adicional.
Esta classe substitui as propriedades existentes por valores da classe recipiente de ensaio. As três primeiras propriedades são propriedades padrão do Spring. As cinco seguintes são propriedades adicionais e personalizadas que podem ser utilizadas para configurar outros recursos e extensões como o liquibase, por exemplo:
Agora é altura de definir um teste de integração simples.
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureTestDatabase(replace = NONE)
@ContextConfiguration(initializers = ContainerInit.class)
@ContentoresTeste
classe DummyRepositoryTest {
@Autowired
private DummyRepository;
@Teste
void shouldReturnDummy() {
var byId = dummyRepository.getById(10L);
var expected = new Dummy();
expected.setId(10L);
assertThat(byId).completes().emitsCount(1).emits(expected);
}
}
Temos aqui algumas anotações extra.
@SpringBootTest(webEnvironment = RANDOM_PORT) - marca o teste como um teste Spring Boot e inicia o contexto do Spring.
@AutoConfigureTestDatabase(replace = NONE) - estas anotações dizem que a extensão spring test não deve substituir a configuração da base de dados postgres por H2 na configuração da memória.
@ContextConfiguration(initializers = ContainerInit.class) - um contexto de mola adicional onde definimos as propriedades de Contentores de ensaio.
@Testcontainers - como mencionado anteriormente, esta anotação controla o ciclo de vida do contentor.
Neste exemplo, utilizo repositórios reactivos, mas funciona da mesma forma com repositórios JDBC e JPA comuns.
Agora podemos executar este teste. Se for a primeira execução, o mecanismo precisa extrair imagens do docker.hub. Isso pode demorar um pouco. Depois disso, veremos que dois containers foram executados. Um é o postgres e o outro é o controlador Testcontainers. Esse segundo contêiner gerencia os contêineres em execução e, mesmo que a JVM pare inesperadamente, ele desliga os contêineres e limpa o ambiente.
Resumindo
Contentores de ensaio são ferramentas muito fáceis de utilizar que nos ajudam a criar testes de integração que utilizam contentores Docker. Isto dá-nos mais flexibilidade e aumenta a velocidade de desenvolvimento. A definição correta da configuração dos testes reduz o tempo necessário para integrar novos programadores. Eles não precisam de configurar todas as dependências, basta executar os testes escritos com ficheiros de configuração selecionados.