13 de novembro de 2022

Use Testcontainers, mas cuidado com o Graceful Stop!

Use Testcontainers mas cuidado com a falta de graceful stop! Testcontainers são excelentes para usar em seus testes, seja com containers pré-prontos, genéricos, ou até mesmo com suas próprias imagens, mas cuidado com a falta de graceful stop!

Testcontainers em Java

É extremamente comum hoje em dia o uso de Testcontainers para testes integrados com Java. Se você ainda não usa esse framework, saiba que está perdendo uma grande oportunidade de melhorar seus testes, então sugiro dar uma olhada aqui!

Os testcontainer são basicamente containers docker* (ou similares) que executam, em geral, algum sistema dentro do seu próprio teste! Então, por exemplo, se você precisa de um banco de dados, ou um Kafka, ou até mesmo simular o ambiente da AWS, pode rodar tudo isso dentro um container diretamente do seus testes!

É uma mão na roda para testes que precisariam de um ambiente externo, e hoje em dia é quase uma obrigação para ter pipelines mais confiáveis e estáveis. Seu teste não irá falhar, por exemplo, por alguma variável externa que foi mal configurada ou alterada, simplesmente porque não há mais dependência de um ambiente externo.

Se você usa Quarkus, por exemplo, é quase natural criar testes com testcontainers! (E se você ainda não conhece o Quarkus, o que você está fazendo?! Já deixa a aba aberta aí pra conhecer depois!)

Containers pré-prontos

Existe uma série de Testcontainers pré-prontos, que já possuem uma classe Java criada para você usar facilmente nos seus testes. Inclusive, esse é o jeito mais comum de usar Testcontainers: entrar no site deles, olhar quais são os módulos disponibilizados, e usar! Para as coisas mais comuns, tipo Kafka, Elasticsearch, RabbitMQ, e inúmeros bancos de dados, já existem Testcontainers prontos para uso!

@Testcontainers
public class BancoDeDadosTest {

    // instanciamos nosso container
    @Container
    private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer()
        .withDatabaseName("foo")
        .withUsername("foo")
        .withPassword("secret");

    @Before
    public void setup() {
        // inicializamos nossas propriedades de ambiente
        System.setProperty("db.port", postgresqlContainer.getFirstMappedPort().toString());
        System.setProperty("db.databaseName", postgresqlContainer.getDatabaseName());
        System.setProperty("db.username", postgresqlContainer.getUsername());
        System.setProperty("db.password", postgresqlContainer.getPassword());
    }

    // fazemos nosso teste
    @Test
    void test() {
        assertThat(postgresqlContainer.isRunning()).isTrue();
    }
}

O código acima foi adaptado da documentação oficial: https://www.testcontainers.org/

Containers genéricos

Também há a possibilidade de usar containers genéricos e transformá-los em Testcontainers. Isso quer dizer: pegar qualquer container docker, por exemplo do Redis, e iniciar um GenericContainer com essa imagem! Nesse caso é comum você precisar configurar algumas coisas a mais, como mapear a porta que precisará ser exposta, ou aplicar algumas variáveis de ambiente. Nesse caso é importante conhecer a imagem que você está usando e ler sua documentação, pois você irá montar todos os parâmetros necessários para essa imagem rodar (assim como você faria se fosse rodar a mesma imagem localmente no seu docker).

@Testcontainers
public class RedisBackedCacheIntTest {

    // nosso cache que é baseado internamente em Redis
    private MeuCacheEmRedis meuCacheEmRedis;

    // instanciamos nosso container
    @Container
    public GenericContainer redisTestcontainer = new GenericContainer(DockerImageName.parse("redis:5.0.3-alpine")).withExposedPorts(6379);

    @BeforeEach
    public void setUp() {
        String endereco = redisTestcontainer.getHost();
        Integer porta = redisTestcontainer.getFirstMappedPort();

        // aqui já temos o endereço e a porta para o Redis que está rodando no container
        // então podemos criar nosso cache apontando para esse Redis
        redisSendoTestado = new MeuCacheEmRedis(endereco, porta);
    }

    // fazemos nosso teste
    @Test
    public void testPutAndGet() {
        meuCacheEmRedis.put("test", "exemplo");

        String valorRecebido = meuCacheEmRedis.get("test");
        assertThat(valorRecebido).isEqualTo("exemplo");
    }
}

O código acima foi adaptado da documentação oficial: https://www.testcontainers.org/

Seus próprios Testcontainers

Bom, se você pode configurar qualquer imagem docker* para usar em seus Testcontainers, então você pode inclusive usar a sua própria imagem, certo? Certo! Se seu sistema gera imagens docker, e você precisa, digamos, testar um módulo do sistema que depende de outro, você pode rodar um Testcontainer dentro do seu teste, com a imagem do módulo dependente! Basta fazer como foi feito no exemplo acima, porém usando sua própria imagem e configurando suas próprias portas e variáveis de ambiente.

Isso é uma ferramenta excelente para testar sistemas com vários módulos!

O problema do Graceful Stop

Use Testcontainers mas cuidado com a falta de graceful stop! É comum que os containers pré-prontos do Testcontainers não usem o graceful stop! Mas o que seria o graceful stop?

Basicamente, quando você pede para o docker parar um container, você tem duas opções: usar o docker kill ou o docker stop. O primeiro irá matar o container imediatamente, sem esperar que ele feche possíveis recursos externos ou execute algum código de finalização. O segundo irá enviar um pedido de stop, aguardar alguns segundos para que o container processe o pedido de stop, e somente caso o container não finalize dentro de alguns segundos ele irá matá-lo forçadamente.

Bom, quando você faz a chamada para matar o container usando as classes Java do Testcontainers, por padrão ele irá usar o docker kill. Em geral, isso não é um problema, pois todos os processos do container serão mortos, e geralmente o Testcontainer não se comunica com o mundo externo, então teoricamente não há grande risco de recursos ficarem abertos.

Para dar uma espiada no código do próprio Testcontainer, abaixo está o trecho de código que eu extraí da chamada para o método stop() da classe GenericContainer, que é a classe base para todos os outros Testcontainers.

        if (running) {
            try {
                LOGGER.trace("Stopping container: {}", containerId);
                dockerClient.killContainerCmd(containerId).exec();
                LOGGER.trace("Stopped container: {}", imageName);
            } catch (Exception e) {
                ...
            }
        }

Isso, porém, pode ser um problema caso você esteja rodando sua própria imagem, ou uma imagem genérica, e a aplicação dessa imagem realizar comunicação com algum ambiente externo, pois é possível que o uso de docker kill deixe esse ambiente externo em um estado inconsistente. Para evitar isso, o ideal é que você sobrescreva a classe GenericContainer com sua própria classe, e faça override do método containerIsStopping solicitando a execução do comando docker stop ao invés do docker kill.

public class MeuContainer extends GenericContainer<MeuContainer> {

    // ...

    @Override
    protected void containerIsStopping(InspectContainerResponse containerInfo) {
        super.containerIsStopped(containerInfo);
        try ( StopContainerCmd cmd = getDockerClient()
                 .stopContainerCmd(getContainerId())
                 .withTimeout(10) ) {
            cmd.exec();
        } catch (Exception e) {
            // ...
        }
    }

}

Isso irá garantir que, pelo menos, o container tenha alguns segundos para tentar parar com sucesso, antes de ser forçadamente parado!


E aí, você já conhecia Testcontainers? Já usa no seu projeto ou no seu trabalho? Gostou desse artigo?

Deixe seu comentário, compartilhe nas suas redes (e me marque!) e vamos espalhar a palavra dos Testcontainers!

Veja outros posts, meus cursos gratuitos, ou meus últimos vídeos!


* Em vários momentos do texto eu cito o docker, porém hoje já existe um padrão aberto de imagens para containers, é o Open Container Initiative (OCI). Como ele é retrocompatível com docker, e “docker” é um nome mais conhecido, eu usei ele durante o texto, mas saiba que tudo isso poderia ser feito com o podman ou moby, por exemplo.

Share