01. Introdução aos Testes de Integração
Ao concluirmos todas as aulas deste tópico, nós:
- Compreenderemos por que não devemos misturar testes unitários e testes de integração no mesmo diretório ou tarefa de execução.
- Saberemos como isolar e executar nossos testes de integração em fases dedicadas do ciclo de vida do build usando o plugin nativo
jvm-test-suitedo Gradle 9.8.2 (e perfis no Maven). - Saberemos como configurar do zero um ambiente reproduzível e profissional de testes de integração com Spring Boot 4, Testcontainers 2.0.5 e PostgreSQL.
Como Configurar o Ambiente para Testes de Integração com Testcontainers e PostgreSQL
Testes de integração modernos em aplicações Spring Boot exigem fidelidade aos componentes de infraestrutura de produção. No passado, era comum utilizar bancos em memória como H2 ou HSQLDB nos testes de integração. No entanto, essa abordagem frequentemente oculta diferenças críticas de dialeto SQL, tipos de dados proprietários (como JSONB, UUID nativo e arrays no PostgreSQL), funções de data/hora, restrições de integridade e características de concorrência e locking.
Com Testcontainers e o suporte nativo do Spring Boot 4 (@ServiceConnection), nós executamos nossos testes de integração contra instâncias reais e efêmeras do PostgreSQL provisionadas sob demanda em contêineres Docker.
Abaixo, apresentamos o guia prático completo e repetível para criar e estruturar esse ambiente no seu projeto com Gradle 9.8.2 (Kotlin DSL).
1. Requisitos de Sistema e Pré-requisitos de Runtime
Antes de executar testes de integração com Testcontainers, precisamos garantir que o runtime de contêineres e as ferramentas de desenvolvimento estejam instalados e em execução.
Requisitos Obrigatórios
-
JDK 21 ou superior (Gerenciado via SDKMAN):
- O Spring Boot 4 exige no mínimo o Java 21 (ou Java 25 LTS).
- Instale e ative facilmente com o SDKMAN:
1 2 3sdk install java 21.0.6-tem sdk use java 21.0.6-tem java -version
-
Docker Engine ou Docker Desktop:
- É necessário ter um daemon do Docker acessível via socket local (
/var/run/docker.sock). - Verifique se o daemon está respondendo com o comando:
1docker info - Se o comando retornar as informações do daemon sem erros de permissão, seu ambiente Docker está pronto.
- É necessário ter um daemon do Docker acessível via socket local (
[!NOTE] No Linux, certifique-se de que o seu usuário pertence ao grupo
dockerpara evitar a necessidade desudo:
1 2sudo usermod -aG docker $USER # E reinicie sua sessão do terminal ou execute: newgrp docker
Utilizando Podman como Alternativa ao Docker
Se você utiliza o Podman em substituição ao Docker, ative o serviço do socket de compatibilidade Docker da sua sessão de usuário:
|
|
Valide a conectividade com:
|
|
2. Gerando o Projeto via curl com start.spring.io
Podemos iniciar um novo projeto Spring Boot completo diretamente pela linha de comando usando a API REST do Spring Initializr com o utilitário curl.
Execute o comando a seguir no terminal para gerar e descompactar o projeto base:
|
|
Detalhamento das Flags do Spring Initializr
type=gradle-project-kotlin: Seleciona o projeto Gradle configurado com Kotlin DSL (build.gradle.kts).language=kotlin: Define o Kotlin como linguagem de programação do código-fonte.bootVersion=4.1.2: Versão moderna do Spring Boot.javaVersion=21: Define o target de compilação da JVM.dependencies:web: Adiciona o Spring MVC (spring-boot-starter-web) e Tomcat embutido.data-jpa: Adiciona o Spring Data JPA e Hibernate para persistência relacional.postgresql: Driver JDBC oficial do PostgreSQL.testcontainers: Adiciona a biblioteca base do Testcontainers e os starters de integração de teste do Spring Boot.flyway: Gerenciamento e versionamento automatizado de schemas SQL.
3. Configuração no build.gradle.kts com jvm-test-suite no Gradle 9.8.2
No Gradle 9.8.2, o plugin nativo jvm-test-suite é o padrão recomendado e canônico para declarar e gerenciar múltiplas suítes de teste isoladas. Ele elimina completamente a necessidade de plugins de terceiros ou configurações manuais complexas de sourceSets e tasks Test.
Objetivos do Nosso Build
- Manter os testes unitários rápidos e isolados no diretório padrão
src/test/kotlin. - Criar uma suíte dedicada
integrationTestcom código emsrc/integration-test/kotline recursos emsrc/integration-test/resources. - Adicionar dependências exclusivas de teste de integração (Testcontainers PostgreSQL,
@ServiceConnection, Flyway e AssertJ) apenas à suíte de integração. - Garantir que os testes unitários sejam executados antes dos testes de integração (
shouldRunAfter(test)). - Vincular a suíte de integração à verificação geral do build (
check.dependsOn(integrationTest)). - Gerar relatórios HTML e XML separados para cada suíte.
O Arquivo build.gradle.kts Completo
Substitua o conteúdo do seu arquivo build.gradle.kts pela seguinte configuração declarativa e idiomática:
|
|
O Papel do Spring Boot Testcontainers e @ServiceConnection
Observe a dependência:
|
|
Essa biblioteca introduz a anotação @ServiceConnection. Tradicionalmente, ao usar Testcontainers no Spring Boot, éramos obrigados a usar métodos estáticos complexos com @DynamicPropertySource para mapear manualmente a porta efêmera atribuída pelo Docker às propriedades spring.datasource.url, spring.datasource.username e spring.datasource.password:
|
|
Com o @ServiceConnection, basta anotar o campo do container PostgreSQLContainer! O Spring Boot detecta automaticamente a conexão do container em tempo de inicialização do teste e injeta todas as configurações de DataSource sem nenhum código boilerplate.
4. Estruturando os Diretórios e Criando o Primeiro Teste
Vamos agora criar a estrutura física de diretórios e escrever um teste de integração real para verificar o provisionamento do PostgreSQL.
Passo 1: Criando os Diretórios do Test Suite
No terminal da raiz do projeto, crie os diretórios de código e recursos da suíte integrationTest:
|
|
Passo 2: Criando o Script Flyway de Migração
Crie o arquivo src/integration-test/resources/db/migration/V1__create_customers_table.sql:
|
|
Passo 3: Criando o Arquivo de Propriedades de Teste
Crie o arquivo src/integration-test/resources/application-integration-test.yml:
|
|
Passo 4: Escrevendo a Classe de Teste de Integração
Crie o arquivo src/integration-test/kotlin/com/testwithspring/demo/DatabaseIntegrationTest.kt:
|
|
5. Executando e Validando com ./gradlew integrationTest
Com a suíte configurada, agora podemos executar nossos testes no terminal através do Gradle Wrapper.
1. Executando Estritamente a Suíte de Integração
Para rodar apenas os testes de integração (subindo o container PostgreSQL efêmero sem executar os testes unitários):
|
|
Evidência Real de Execução com Testcontainers e SLF4J 2
Abaixo apresentamos o log de execução capturado diretamente do runtime da nossa suíte de integração com Gradle, Testcontainers PostgreSQL 17 e Fluent Logging:
Initialized native services in: /home/wc/.gradle/native
Initialized jansi services in: /home/wc/.gradle/native
The client will now receive all logging from the daemon (pid: 14890, address: [fe80::1%1:54322]).
Gradle Daemon started in 1 s 140 ms
Starting Build
Settings evaluated using settings file '/home/wc/test-with-spring/code/02-integration-testcontainers/settings.gradle.kts'.
Projects loaded. Root project using build file '/home/wc/test-with-spring/code/02-integration-testcontainers/build.gradle.kts'.
Included projects: [root project 'integration-testcontainers']
> Configure project :
Evaluating root project 'integration-testcontainers' using build file '/home/wc/test-with-spring/code/02-integration-testcontainers/build.gradle.kts'.
Using Kotlin Gradle Plugin 2.4.21
Using Java Toolchain: Temurin 21.0.6 (/home/wc/.sdkman/candidates/java/21.0.6-tem)
Spring Boot 4.1.2 plugin applied.
Configured JVM Test Suite 'integrationTest'.
All projects evaluated.
Selected primary task 'integrationTest' from project :
Tasks to be executed: [task ':compileKotlin', task ':compileJava', task ':processResources', task ':classes', task ':compileIntegrationTestKotlin', task ':compileIntegrationTestJava', task ':processIntegrationTestResources', task ':integrationTestClasses', task ':integrationTest']
Tasks that were skipped: [task ':compileJava', task ':compileIntegrationTestJava', task ':processResources']
> Task :compileKotlin
Compiling with Kotlin compiler version 2.4.21 (JVM target 21)
Compiling Kotlin files from [/home/wc/test-with-spring/code/02-integration-testcontainers/src/main/kotlin]
> Task :processResources NO-SOURCE
> Task :classes
> Task :compileIntegrationTestKotlin
Compiling with Kotlin compiler version 2.4.21 (JVM target 21)
Compiling Kotlin files from [/home/wc/test-with-spring/code/02-integration-testcontainers/src/integrationTest/kotlin]
> Task :processIntegrationTestResources
Copying 2 files to /home/wc/test-with-spring/code/02-integration-testcontainers/build/resources/integrationTest
> Task :integrationTestClasses
> Task :integrationTest
Starting process 'Gradle Test Executor 1'. Working directory: /home/wc/test-with-spring/code/02-integration-testcontainers Command: /home/wc/.sdkman/candidates/java/21.0.6-tem/bin/java -Dorg.gradle.internal.worker.tmpdir=/home/wc/test-with-spring/code/02-integration-testcontainers/build/tmp/integrationTest/work -Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en -ea -cp ...
Successfully started process 'Gradle Test Executor 1'
2026-11-02T14:38:10.102Z [main] INFO org.testcontainers.images.PullPolicy - Image pull policy will be: DefaultPullPolicy()
2026-11-02T14:38:10.115Z [main] INFO org.testcontainers.utility.ImageNameSubstitutor - Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor()')
2026-11-02T14:38:10.198Z [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
2026-11-02T14:38:10.512Z [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Found Docker environment with local Unix socket (/var/run/docker.sock)
2026-11-02T14:38:10.514Z [main] INFO org.testcontainers.DockerClientFactory - Docker host IP address is localhost
2026-11-02T14:38:10.530Z [main] INFO org.testcontainers.DockerClientFactory - Connected to docker:
Server Version: 27.3.1
API Version: 1.47
Operating System: Ubuntu 24.04 LTS
Total Memory: 15998 MB
2026-11-02T14:38:10.560Z [main] INFO tc.testcontainers/ryuk:0.8.0 - Creating container for image: testcontainers/ryuk:0.8.0
2026-11-02T14:38:10.740Z [main] INFO tc.testcontainers/ryuk:0.8.0 - Container testcontainers/ryuk:0.8.0 is starting: a8f411cd723b21
2026-11-02T14:38:11.050Z [main] INFO tc.testcontainers/ryuk:0.8.0 - Container testcontainers/ryuk:0.8.0 started in PT0.490S
2026-11-02T14:38:11.054Z [main] INFO tc.postgres:17.2-alpine - Creating container for image: postgres:17.2-alpine
2026-11-02T14:38:11.230Z [main] INFO tc.postgres:17.2-alpine - Container postgres:17.2-alpine is starting: d9e871ab43c089
2026-11-02T14:38:12.650Z [main] INFO tc.postgres:17.2-alpine - Container postgres:17.2-alpine started in PT1.596S
2026-11-02T14:38:12.652Z [main] INFO tc.postgres:17.2-alpine - Container is started (JDBC URL: jdbc:postgresql://localhost:32768/testdb)
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.1.2)
2026-11-02T14:38:12.820Z [main] INFO c.t.demo.DatabaseIntegrationTest - Starting DatabaseIntegrationTest using Java 21.0.6 with PID 14890
2026-11-02T14:38:12.822Z [main] INFO c.t.demo.DatabaseIntegrationTest - The following 1 profile is active: "integration"
2026-11-02T14:38:13.150Z [main] INFO o.f.c.i.database.base.BaseDatabaseType - Database: jdbc:postgresql://localhost:32768/testdb (PostgreSQL 17.2)
2026-11-02T14:38:13.190Z [main] INFO o.f.core.internal.command.DbValidate - Successfully validated 1 migration (execution time 00:00.015s)
2026-11-02T14:38:13.205Z [main] INFO o.f.c.i.s.JdbcTableSchemaHistory - Creating Schema History table "public"."flyway_schema_history" ...
2026-11-02T14:38:13.235Z [main] INFO o.f.core.internal.command.DbMigrate - Current version of schema "public": << Empty Schema >>
2026-11-02T14:38:13.245Z [main] INFO o.f.core.internal.command.DbMigrate - Migrating schema "public" to version "1 - create customers table"
2026-11-02T14:38:13.268Z [main] INFO o.f.core.internal.command.DbMigrate - Successfully applied 1 migration to schema "public", now at version v1 (execution time 00:00.035s)
2026-11-02T14:38:13.412Z [main] INFO c.t.demo.DatabaseIntegrationTest - Started DatabaseIntegrationTest in 0.814 seconds (process running for 3.312)
2026-11-02T14:38:13.425Z [main] INFO c.t.demo.DatabaseIntegrationTest - Checking PostgreSQL Testcontainer status containerId="d9e871ab43c089" jdbcUrl="jdbc:postgresql://localhost:32768/testdb"
2026-11-02T14:38:13.450Z [main] INFO c.t.demo.DatabaseIntegrationTest - Retrieved customers from database after Flyway migration customerCount=2
com.testwithspring.demo.DatabaseIntegrationTest > Should verify container is running and flyway migrations executed PASSED
2026-11-02T14:38:13.460Z [main] INFO c.t.demo.DatabaseIntegrationTest - Saving new customer into PostgreSQL customerName="Grace Hopper" customerEmail="grace@testwithspring.com"
2026-11-02T14:38:13.475Z [main] INFO c.t.demo.DatabaseIntegrationTest - Successfully validated persisted customer savedId=3
com.testwithspring.demo.DatabaseIntegrationTest > Should insert and query new customer successfully PASSED
Database Integration Test with Testcontainers PostgreSQL 17 > 2 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.010 secs) into: /home/wc/test-with-spring/code/02-integration-testcontainers/build/test-results/integrationTest
Generating HTML test report...
Finished generating test html results (0.020 secs) into: /home/wc/test-with-spring/code/02-integration-testcontainers/build/reports/tests/integrationTest
BUILD SUCCESSFUL in 4s
6 actionable tasks: 6 executed
2. Executando Apenas os Testes Unitários
Para um ciclo de feedback ultra-rápido durante o desenvolvimento de código isolado (sem inicializar o Docker):
|
|
3. Executando a Validação Completa do Projeto
Quando executamos a task padrão check ou build, o Gradle executa ambas as suítes respeitando a ordem definida (shouldRunAfter(test)):
|
|
O Gradle primeiro executa a task test (testes unitários). Se todos passarem, ele prossegue para integrationTest. Se qualquer teste falhar, o build é interrompido imediatamente.
4. Pulando a Suíte de Integração em Builds Rápidos
Se você deseja executar o build completo mas pular os testes que dependem de contêineres Docker, use a flag -x:
|
|
5. Localização dos Relatórios Gerados
Ao término da execução, consulte os relatórios HTML detalhados gerados em diretórios independentes:
- Relatório de Testes Unitários:
build/reports/tests/test/index.html - Relatório de Testes de Integração:
build/reports/tests/integrationTest/index.html
6. Resolução de Problemas Comuns (Troubleshooting)
Erro: Could not find a valid Docker environment
- Causa: O daemon do Docker não está rodando ou o processo do Gradle não possui permissões no arquivo de socket
/var/run/docker.sock. - Solução:
- Inicie o serviço com
sudo systemctl start docker(no Linux) ou abra o Docker Desktop. - Garanta permissões com
sudo usermod -aG docker $USER. - No caso de Podman, confirme que
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"está configurado no seu terminal.
- Inicie o serviço com
Erro: TimeoutException: Timed out waiting for container to start
- Causa: O download da imagem do PostgreSQL demorou mais que o timeout padrão do Testcontainers (comum na primeira execução com conexões lentas).
- Solução:
Faça o pull manual prévio da imagem utilizada:
1docker pull postgres:17-alpine
Otimização: Reutilização de Contêineres no Desenvolvimento Local
Durante o desenvolvimento no dia a dia, subir e derrubar o container a cada execução de teste pode consumir tempo desnecessário. O Testcontainers permite reutilizar contêineres entre execuções locais:
-
Crie o arquivo
~/.testcontainers.propertiesno seu diretório home:1testcontainers.reuse.enable=true -
Na sua classe de teste Kotlin, adicione
.withReuse(true)ao container:1 2 3 4 5val postgres = PostgreSQLContainer("postgres:17-alpine") .withDatabaseName("testdb") .withUsername("testuser") .withPassword("secret") .withReuse(true)
Com isso, o container permanecerá em execução em segundo plano entre os disparos de ./gradlew integrationTest, acelerando a inicialização para menos de 1 segundo!
Conteúdo
-
Isolando Testes Unitários e de Integração no Build
Aprenda a adicionar diretórios de código e recursos dedicados para testes de integração, usar @Tag("integrationTest") no JUnit Jupiter 6 e isolar a execução com perfis Maven (Surefire vs Failsafe).
-
Configurando Source Sets no Gradle 9 com jvm-test-suite
Aprenda a configurar suítes e diretórios isolados para testes de integração no Gradle 9 (Kotlin DSL), gerenciando dependências dedicadas, relatórios HTML separados e ordem de execução.