Introdução ao Testcontainers 2.0

Ao final desta aula, seremos capazes de:

  • Entender o que é o Testcontainers.
  • Obter as dependências necessárias com o Gradle (Kotlin DSL).
  • Compreender como utilizar contêineres genéricos (GenericContainer).
  • Executar um banco de dados PostgreSQL com o Testcontainers (PostgreSQLContainer).
  • Saber como executar um contêiner do Selenium WebDriver (BrowserWebDriverContainer).

Obtenha o código-fonte no GitHub

O Que É o Testcontainers?

O Testcontainers nos permite iniciar um contêiner Docker (ou runtime OCI compatível) antes que nossos métodos de teste sejam executados e encerrá-lo após a conclusão dos testes. Na stack moderna do Spring Boot 4.1.2, ele requer um ambiente compatível com a API do Docker (Docker Engine, Docker Desktop, OrbStack, Rancher Desktop ou Podman), Java 21+ / Kotlin 2.4.21 e JUnit Jupiter 6.0.3 (no curso original de 2018, exigia Docker ou docker-machine, Java 8 e JUnit 4). É possível utilizar o Testcontainers no Linux, macOS e Windows com suporte de primeira classe em todos os sistemas operacionais modernos.

A biblioteca Testcontainers possui cinco modos principais de uso:

  • Podemos executar um contêiner genérico (GenericContainer).
  • Podemos executar um contêiner Docker que inicia um banco de dados relacional ou NoSQL. O Testcontainers suporta dezenas de bancos de dados dedicados, incluindo PostgreSQL, MySQL, MariaDB, Oracle XE/Free, MongoDB, Redis e muitos outros.
  • Podemos iniciar um contêiner Docker que executa um navegador web. Após iniciar esse contêiner, podemos controlar o navegador web utilizando um RemoteWebDriver do Selenium.
  • Podemos executar contêineres que iniciam os serviços definidos em um arquivo Docker Compose (ComposeContainer).
  • Podemos executar um contêiner Docker criado dinamicamente a partir de um Dockerfile (ImageFromDockerfile).

[!NOTE] Esta aula concentra-se nos três primeiros modos de uso. Se você quiser obter mais informações sobre os modos de uso avançados, consulte a Documentação Oficial do Testcontainers para Java/Kotlin.

A seguir, descobriremos como obter as dependências necessárias com o Gradle.

Obtendo as Dependências Necessárias com o Gradle

Podemos obter as dependências necessárias no Gradle 9.8.2 (Kotlin DSL) adicionando os seguintes módulos ao nosso arquivo build.gradle.kts (gerenciados pela BOM do Spring Boot 4.1.2 ou pela BOM do Testcontainers 2.0.5):

  • O módulo testcontainers contém a funcionalidade central (core) e fornece suporte para contêineres genéricos e Docker Compose.
  • O módulo junit-jupiter fornece a integração declarativa com o ciclo de vida do JUnit Jupiter 6 (@Testcontainers e @Container).
  • O módulo postgresql fornece suporte especializado para a execução do banco de dados PostgreSQL.
  • O módulo selenium fornece suporte para a execução de contêineres Selenium WebDriver.

Em outras palavras, podemos obter as dependências necessárias adicionando o seguinte trecho à seção dependencies do nosso arquivo build.gradle.kts:

1
2
3
4
5
6
7
8
9
dependencies {
    testImplementation(platform("org.testcontainers:testcontainers-bom:2.0.5"))
    testImplementation("org.testcontainers:testcontainers")
    testImplementation("org.testcontainers:junit-jupiter")
    testImplementation("org.testcontainers:postgresql")
    testImplementation("org.testcontainers:selenium")
    testImplementation("org.seleniumhq.selenium:selenium-java")
    testImplementation("org.postgresql:postgresql")
}

[!NOTE] Leitura Adicional:

Vamos avançar e descobrir como podemos configurar o ciclo de vida do contêiner Docker iniciado.

Configurando o Contêiner Docker Iniciado

Na stack moderna com JUnit Jupiter 6.0.3 e Testcontainers 2.0.5, configuramos o ciclo de vida do contêiner Docker anotando nossa classe de teste com @Testcontainers e utilizando a anotação @Container (que substitui as antigas regras @ClassRule e @Rule do JUnit 4 usadas na versão original do curso). Quando configuramos o contêiner Docker iniciado, temos duas opções principais:

  • Se quisermos que o contêiner Docker seja iniciado apenas uma vez antes da execução de qualquer método de teste da classe (equivalente ao antigo @ClassRule do JUnit 4), devemos declarar uma propriedade dentro do companion object da nossa classe de teste (com @JvmStatic) e anotá-la com @Container.
  • Se quisermos que um novo contêiner Docker seja iniciado antes de cada método de teste e destruído logo após (equivalente ao antigo @Rule do JUnit 4), devemos declarar uma propriedade de instância na nossa classe de teste e anotá-la com @Container.

[!NOTE] Na maioria das vezes, devemos iniciar nossos contêineres Docker apenas uma vez por classe (ou de forma compartilhada na suíte), pois iniciá-los antes de cada método de teste pode tornar a execução da nossa suíte de testes excessivamente lenta. É por isso que todos os exemplos desta aula iniciam o contêiner Docker apenas uma vez no companion object.

A seguir, veremos como podemos executar contêineres genéricos.

Executando Contêineres Genéricos

Se quisermos executar um contêiner genérico, devemos utilizar a classe GenericContainer. Ao criar um novo objeto GenericContainer, passamos o nome da imagem Docker (DockerImageName) como argumento do construtor. Depois de criar o objeto GenericContainer, podemos configurar o contêiner Docker iniciado utilizando os métodos fluentes da classe GenericContainer.

Vamos escrever um método de teste que utiliza um contêiner Docker executando uma imagem mínima baseada no Alpine Linux. Esse contêiner Docker expõe a porta 80. Quando enviamos uma requisição HTTP para a porta exposta, o contêiner Docker iniciado retorna o código de status HTTP 200 e a mensagem: 'TestContainers Rules!'.

Podemos escrever esse método de teste seguindo estes passos:

Primeiro, devemos configurar a instância de GenericContainer que inicia o nosso contêiner Docker. Ao configurar esse contêiner, precisamos definir a porta exposta (withExposedPorts) e o comando (withCommand) que retorna a resposta HTTP contendo a mensagem esperada.

Após configurarmos o GenericContainer, o código-fonte da nossa classe de teste fica da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.utility.DockerImageName

@Testcontainers
class ClassRuleGenericContainerTest {

    companion object {
        @Container
        @JvmStatic
        val webServer: GenericContainer<*> = GenericContainer(
            DockerImageName.parse("alpine:3.20")
        )
            .withExposedPorts(80)
            .withCommand(
                "/bin/sh",
                "-c",
                "while true; do echo -e \"HTTP/1.1 200 OK\r\n\r\nTestContainers Rules!\" | nc -l -p 80; done"
            )
    }
}

Segundo, devemos escrever um método de teste que garanta que o nosso contêiner Docker retorne a mensagem correta quando enviamos uma requisição GET para a porta exposta (utilizando o HttpClient nativo do Java 21+ ou cliente HTTP equivalente).

Após escrevermos o nosso método de teste, o código-fonte completo da nossa classe de teste fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import org.testcontainers.utility.DockerImageName
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

@Testcontainers
class ClassRuleGenericContainerTest {

    companion object {
        @Container
        @JvmStatic
        val webServer: GenericContainer<*> = GenericContainer(
            DockerImageName.parse("alpine:3.20")
        )
            .withExposedPorts(80)
            .withCommand(
                "/bin/sh",
                "-c",
                "while true; do echo -e \"HTTP/1.1 200 OK\r\n\r\nTestContainers Rules!\" | nc -l -p 80; done"
            )
    }

    @Test
    fun shouldReturnCorrectMessage() {
        val httpClient = HttpClient.newHttpClient()
        val request = HttpRequest.newBuilder()
            .uri(URI.create("http://${webServer.host}:${webServer.getMappedPort(80)}"))
            .GET()
            .build()

        val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())

        val greeting = response.body().trim()
        assertThat(greeting).isEqualTo("TestContainers Rules!")
    }
}

[!NOTE] Quero destacar dois pontos importantes antes de continuarmos esta aula:

  • Se quisermos obter o endereço de host/IP do contêiner Docker iniciado, devemos acessar a propriedade host (método getHost(), que substituiu o antigo getContainerIpAddress() depreciado nas versões antigas do Testcontainers) da classe GenericContainer.
  • Se quisermos obter a porta real no host que nos permite conectar à porta exposta pelo contêiner, devemos invocar o método getMappedPort() da classe GenericContainer. Ao invocar esse método, devemos passar o número da porta original exposta pelo contêiner como parâmetro.

Leitura Adicional:

Vamos avançar e descobrir como podemos executar bancos de dados PostgreSQL com o Testcontainers.

Executando um Banco de Dados PostgreSQL

Se quisermos executar um contêiner que inicia um banco de dados PostgreSQL, devemos utilizar a classe PostgreSQLContainer. A classe PostgreSQLContainer permite especificar a imagem Docker do PostgreSQL no construtor:

  • No Testcontainers clássico (1.8.0), o construtor sem argumentos iniciava o PostgreSQL 9.6.8.
  • No Testcontainers 2.0.5, especificamos explicitamente a versão desejada do banco de dados PostgreSQL passando o nome da imagem Docker (por exemplo, "postgres:16-alpine" ou DockerImageName.parse("postgres:16-alpine")) como argumento do construtor.

Depois de criarmos um novo objeto PostgreSQLContainer, podemos configurar o banco de dados iniciado utilizando os métodos da classe PostgreSQLContainer.

Vamos escrever um método de teste que inicia um contêiner PostgreSQL e valida a conexão JDBC. Podemos escrever esse método de teste seguindo estes passos:

Primeiro, devemos configurar a instância de PostgreSQLContainer que inicia o nosso banco de dados PostgreSQL. Após configurarmos o contêiner, o código-fonte da nossa classe de teste fica da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

@Testcontainers
class PostgreSQLContainerTest {

    companion object {
        @Container
        @JvmStatic
        val postgreSQL = PostgreSQLContainer("postgres:16-alpine")
    }
}

Segundo, devemos escrever o método de teste que garante que o banco de dados PostgreSQL foi iniciado com sucesso executando a consulta SELECT 42. Após escrevermos esse método de teste, o código-fonte da nossa classe de teste fica da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.sql.DriverManager

@Testcontainers
class PostgreSQLContainerTest {

    companion object {
        @Container
        @JvmStatic
        val postgreSQL = PostgreSQLContainer("postgres:16-alpine")
    }

    @Test
    fun shouldRunPostgreSQLContainer() {
        DriverManager.getConnection(
            postgreSQL.jdbcUrl,
            postgreSQL.username,
            postgreSQL.password
        ).use { conn ->
            conn.createStatement().use { statement ->
                statement.executeQuery("SELECT 42").use { resultSet ->
                    resultSet.next()
                    val result = resultSet.getInt(1)
                    assertThat(result).isEqualByComparingTo(42)
                }
            }
        }
    }
}

[!NOTE] Como podemos observar, podemos obter todos os detalhes de conexão do banco de dados PostgreSQL iniciado (jdbcUrl, username e password) utilizando as propriedades/métodos getters fornecidos pela classe PostgreSQLContainer.

Leitura Adicional:

A seguir, descobriremos como podemos executar um contêiner do Selenium WebDriver.

Executando um Contêiner do Selenium WebDriver

Se quisermos executar um contêiner do Selenium WebDriver, devemos utilizar a classe BrowserWebDriverContainer. Depois de criarmos um novo objeto BrowserWebDriverContainer, podemos configurar o navegador iniciado utilizando o método withCapabilities() (sucessor moderno do antigo withDesiredCapabilities()) da classe BrowserWebDriverContainer, passando uma instância de ChromeOptions ou FirefoxOptions.

Vamos escrever um método de teste que utiliza o Google Chrome em contêiner para abrir a página inicial do site deste curso. Podemos escrever esse método de teste seguindo estes passos:

Primeiro, devemos configurar a instância de BrowserWebDriverContainer que inicia o contêiner do Selenium WebDriver. Ao criar um novo objeto BrowserWebDriverContainer, garantimos que o contêiner inicie o navegador web Chrome através de ChromeOptions().

Após configurarmos o BrowserWebDriverContainer, o código-fonte da nossa classe de teste fica da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import org.openqa.selenium.chrome.ChromeOptions
import org.testcontainers.containers.BrowserWebDriverContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

@Testcontainers
class SeleniumContainerTest {

    companion object {
        @Container
        @JvmStatic
        val chrome: BrowserWebDriverContainer<*> = BrowserWebDriverContainer()
            .withCapabilities(ChromeOptions())
    }
}

Segundo, devemos escrever um método de teste que abre a página inicial do site deste curso utilizando uma instância de RemoteWebDriver conectada ao endereço Selenium do contêiner e garante que a página aberta possua o título correto.

Após escrevermos esse método de teste, o código-fonte da nossa classe de teste fica da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.remote.RemoteWebDriver
import org.testcontainers.containers.BrowserWebDriverContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

@Testcontainers
class SeleniumContainerTest {

    companion object {
        @Container
        @JvmStatic
        val chrome: BrowserWebDriverContainer<*> = BrowserWebDriverContainer()
            .withCapabilities(ChromeOptions())
    }

    @Test
    fun shouldReturnCorrectTitleOfCoursesWebsite() {
        val driver = RemoteWebDriver(chrome.seleniumAddress, ChromeOptions())
        try {
            driver.get("https://www.testwithspring.com")

            val title = driver.title
            assertThat(title).isEqualTo(
                "Test With Spring Course — Save Time by Writing Less Test Code"
            )
        } finally {
            driver.quit()
        }
    }
}

[!NOTE] Como podemos ver, podemos conectar o RemoteWebDriver invocando chrome.seleniumAddress (ou chrome.webDriver nas APIs clássicas) da classe BrowserWebDriverContainer.

Leitura Adicional:

Agora já sabemos como obter as dependências necessárias com o Gradle e utilizar os modos de uso mais comuns da biblioteca Testcontainers. Vamos resumir o que aprendemos nesta aula.

Resumo

Esta aula nos ensinou cinco coisas:

  • O Testcontainers nos permite iniciar um contêiner Docker antes da execução dos nossos métodos de teste e encerrá-lo após os nossos métodos de teste terem sido executados.
  • Na stack moderna, o Testcontainers 2.0.5 requer um ambiente Docker compatível, Java 21+ / Kotlin 2.4.21 e JUnit Jupiter 6.0.3 (com @Testcontainers e @Container).
  • Se quisermos executar um contêiner genérico, devemos utilizar a classe GenericContainer.
  • Se quisermos executar um contêiner que rode um banco de dados PostgreSQL, devemos utilizar a classe PostgreSQLContainer.
  • Se quisermos executar um contêiner do Selenium WebDriver, devemos utilizar a classe BrowserWebDriverContainer.

Obtenha o código-fonte no GitHub