
Testes integracao spring boot testcontainers: evite falhas em
Quando a meta é ter testes integracao spring boot testcontainers confiáveis, o ponto central não é “subir o contexto e torcer para funcionar”. Por outro lado, é validar controller, service e repository contra um banco real, isolado por execução, para evitar a falsa segurança de testes unitários demais e de mocks que escondem problemas de integração. Se voce quiser comparar essa abordagem com outro cenario comum no ecossistema Spring, vale revisar Como Criar uma API REST com Spring Boot em 15 Minutos [Guia Rápido].
Em projetos Spring Boot, esse tipo de teste costuma revelar falhas que passam despercebidas no ambiente local: SQL incompatível, mapeamento JPA incompleto, transações mal configuradas, serialização diferente da esperada e até problemas de endpoint que só aparecem quando toda a pilha conversa de verdade. Por outro lado, quem já montou uma API REST com Spring Boot sabe que o código pode parecer perfeito no Mockito e ainda falhar no banco de verdade. Ao mesmo tempo, se você quiser revisar a base de uma API antes de chegar nesse nível de teste, vale olhar o conteúdo sobre Como Criar uma API REST com Spring Boot em 15 Minutos [Guia Rápido]. Para complementar esse ponto com um exemplo proximo do dia a dia, consulte Testes Unitários no Spring Boot para Iniciantes: JUnit e Mockito na Prática.
Testes integracao spring boot testcontainers: secao pratica com codigo completo
Na pratica, um exemplo enxuto ajuda a sair da teoria e evitar erro comum de producao quando o projeto cresce. Esse detalhe conversa bem com o que eu mostrei em API REST Spring Boot Java: Guia Completo com Exemplo Prático.
@RestController
@RequestMapping("/api/exemplo")
public class ExemploController {
@GetMapping
public ResponseEntity<String> listar() {
return ResponseEntity.ok("ok");
}
}testes integracao spring boot testcontainers com banco real isolado
Spring Boot com Testcontainers resolve um problema bem comum: o teste precisa de banco, mas você não quer depender de PostgreSQL, MySQL ou MariaDB instalados na máquina de cada dev, nem de um banco compartilhado de homologação. Por outro lado, o container sobe durante a suíte, entrega um banco real e morre no fim. Ao mesmo tempo, resultado: o teste é reproduzível, isolado e muito mais próximo da produção. Se quiser aprofundar o assunto por outro angulo, leia tambem Ia para programadores java backend exemplo: evite erros em.
Isso é diferente de testar com H2 “porque é leve”. Por outro lado, h2 pode ser útil em cenários específicos, mas ele não se comporta exatamente como seu banco real. Ao mesmo tempo, diferenças de dialect, constraints, tipos, funções SQL e estratégia de identidade viram armadilhas. Na prática, se seu time usa PostgreSQL em produção, faz muito mais sentido usar PostgreSQL em teste. Ainda assim, é aí que spring boot testcontainers ganha valor prático. Quando esse tipo de duvida aparece em projeto real, eu costumo voltar neste material: Como tratar exceções no Spring Boot com @ControllerAdvice e @ExceptionHandler.
Em uma arquitetura normal, você tem três camadas que merecem atenção em conjunto: controller, service e repository. Por outro lado, o controller precisa validar entrada e mapear saída corretamente; o service aplica regra de negócio; o repository conversa com o banco. Ao mesmo tempo, quando você testa isso com um banco real em teste spring boot, você verifica o caminho inteiro, sem mascarar erros com mocks excessivos.
Por que testes de integração Spring Boot falham quando ficam unitários demais
Testes unitários têm seu lugar. Por outro lado, eles são rápidos, baratos e bons para validar regras bem isoladas. Ao mesmo tempo, o problema começa quando o time passa a chamar de “cobertura” algo que só exercita mocks e asserts no vazio. Um service pode passar em 20 testes unitários e ainda assim quebrar em produção porque o repository está montado com query errada, o JSON do controller não bate com o contrato ou a transação não faz commit como esperado.
Isso aparece muito em APIs que evoluem rápido. Por outro lado, o dev altera um campo da entidade, ajusta o DTO, troca uma anotação no controller e confia que o teste com Mockito vai capturar tudo. Ao mesmo tempo, não captura. Na prática, o teste unitário normalmente não sobe o contexto web, não abre conexão real com banco e não valida o comportamento do Spring Data JPA com o dialect correto. Ainda assim, em contraste, testes de integracao com testcontainers aproximam bastante o teste do runtime real.
Se a sua base ainda está amadurecendo, vale reforçar a diferença entre teste unitário e integração. Por outro lado, um material útil para isso é Testes Unitários no Spring Boot para Iniciantes: JUnit e Mockito na Prática. Ao mesmo tempo, o ideal não é trocar um pelo outro, mas combinar os dois com intenção. Na prática, unitário para regra pura, integração para borda do sistema.
Erros comuns em testes de integração Spring Boot
O primeiro erro é continuar usando mock onde deveria haver banco real. Por outro lado, um repository mockado em um teste de service não diz quase nada sobre a query, o mapeamento da entidade ou a configuração do Spring Data. Ao mesmo tempo, o segundo erro é subir o contexto inteiro sem controle e deixar a suíte lenta, frágil e difícil de manter. Na prática, o terceiro é usar dados soltos e aleatórios sem reset de estado, criando testes que passam hoje e falham amanhã por causa de ordem de execução.
Outro problema recorrente é misturar responsabilidades. Tem gente que escreve um teste de controller querendo provar regra de negócio complexa e, ao mesmo tempo, validar acesso a dados. Ao mesmo tempo, o teste fica grande, opaco e frágil. Na prática, em um projeto saudável, o teste de integração deve cobrir o fluxo real da aplicação, mas com foco claro: ou você quer validar o endpoint com o banco, ou quer validar o service integrado ao repository, ou quer garantir que a camada de persistência realmente grava e lê certo. Ainda assim, essa separação ajuda a diagnosticar falhas sem caça ao bug.
Em produção, os erros mais caros costumam ser silenciosos. Por outro lado, uma coluna nullable que era obrigatória, uma migration ausente, uma query que funciona no banco local e quebra no ambiente real, um endpoint que devolve status 200 com payload inválido. Ao mesmo tempo, banco real em testes spring boot reduz bastante esse risco, principalmente quando o time já viu situações em que algo “passou nos testes” e falhou no deploy.
Se a sua API ainda não trata bem esses erros, combinar testes com uma estratégia de exceção consistente ajuda muito. Por outro lado, o conteúdo sobre Como tratar exceções no Spring Boot com @ControllerAdvice e @ExceptionHandler complementa bem essa parte.
Exemplo prático completo com Spring Boot e Testcontainers
Abaixo está um exemplo direto, com entidade, repository, service e controller. Por outro lado, a ideia é subir PostgreSQL via Testcontainers e testar o fluxo real da aplicação com testes integracao spring boot testcontainers. Ao mesmo tempo, o exemplo usa JUnit 5, Spring Boot Test e Testcontainers com PostgreSQL.
Dependências principais
No Maven, você precisará do starter de testes, do driver do banco e do módulo de Testcontainers. Por outro lado, em muitos projetos, o driver já vem como dependência de runtime. Ao mesmo tempo, o importante é garantir que o banco usado no teste seja o mesmo da stack de produção quando isso fizer sentido. Na prática, se a aplicação usa PostgreSQL em produção, teste com PostgreSQL.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Entidade e repository
Vamos usar uma entidade simples de cadastro de produto. Por outro lado, o foco aqui não é a modelagem em si, e sim mostrar que a persistência precisa ser validada de ponta a ponta.
package br.com.javalizando.exemplo.domain;
import jakarta.persistence.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer stock;
protected Product() {
}
public Product(String name, Integer stock) {
this.name = name;
this.stock = stock;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Integer getStock() {
return stock;
}
}package br.com.javalizando.exemplo.repository;
import br.com.javalizando.exemplo.domain.Product;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
}Service com regra de negócio simples
O service faz a criação e pode evoluir para regras mais reais, como validação de estoque, normalização de nome ou verificação de duplicidade. Por outro lado, o ponto aqui é não mockar tudo de forma que o teste deixe de dizer algo útil.
package br.com.javalizando.exemplo.service;
import br.com.javalizando.exemplo.domain.Product;
import br.com.javalizando.exemplo.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
@Transactional
public Product create(String name, Integer stock) {
Product product = new Product(name, stock);
return repository.save(product);
}
public Product findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Produto não encontrado"));
}
}Controller REST
O controller expõe os endpoints e conversa com o service. Por outro lado, em testes de integração, vale validar tanto o status quanto o JSON de resposta, porque problema de contrato costuma virar bug de front ou de integração com outros serviços.
package br.com.javalizando.exemplo.controller;
import br.com.javalizando.exemplo.domain.Product;
import br.com.javalizando.exemplo.service.ProductService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) {
this.service = service;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Map<String, Object> create(@RequestBody Map<String, Object> payload) {
Product product = service.create(
(String) payload.get("name"),
(Integer) payload.get("stock")
);
return Map.of(
"id", product.getId(),
"name", product.getName(),
"stock", product.getStock()
);
}
@GetMapping("/{id}")
public Map<String, Object> findById(@PathVariable Long id) {
Product product = service.findById(id);
return Map.of(
"id", product.getId(),
"name", product.getName(),
"stock", product.getStock()
);
}
}Teste de integração com banco real
A seguir, um teste com o contexto Spring Boot completo e PostgreSQL em container. Por outro lado, repare que o banco sobe de forma isolada e cada execução começa do zero. Ao mesmo tempo, isso evita depender de estado local e reduz bugs intermitentes.
package br.com.javalizando.exemplo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.datasource.driver-class-name", postgres::getDriverClassName);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "update");
registry.add("spring.jpa.show-sql", () -> "true");
}
@LocalServerPort
int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateAndFetchProductUsingRealDatabase() {
String baseUrl = "http://localhost:" + port + "/products";
Map<String, Object> payload = Map.of(
"name", "Teclado mecânico",
"stock", 10
);
var createResponse = restTemplate.postForEntity(baseUrl, payload, Map.class);
assertThat(createResponse.getStatusCode().value()).isEqualTo(201);
assertThat(createResponse.getBody()).isNotNull();
assertThat(createResponse.getBody().get("id")).isNotNull();
assertThat(createResponse.getBody().get("name")).isEqualTo("Teclado mecânico");
Number id = (Number) createResponse.getBody().get("id");
var getResponse = restTemplate.getForEntity(baseUrl + "/" + id.longValue(), Map.class);
assertThat(getResponse.getStatusCode().value()).isEqualTo(200);
assertThat(getResponse.getBody()).isNotNull();
assertThat(getResponse.getBody().get("name")).isEqualTo("Teclado mecânico");
assertThat(getResponse.getBody().get("stock")).isEqualTo(10);
}
}Esse teste valida controller, service e repository em conjunto. Por outro lado, se o mapeamento da entidade estiver errado, ele falha. Ao mesmo tempo, se o banco não aceitar a estrutura, ele falha. Na prática, se o controller não serializar certo, ele falha. Ainda assim, se o service tentar salvar dados inválidos, ele falha. Por isso, isso é o tipo de sinal que um teste de integração deveria dar.
Teste de repository com foco mais estreito
Também faz sentido criar um teste menor para o repository, principalmente quando você tem queries customizadas ou relacionamento entre entidades. Por outro lado, em vez de mockar o JPA, você verifica o comportamento real de persistência e leitura.
package br.com.javalizando.exemplo.repository;
import br.com.javalizando.exemplo.domain.Product;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
@DataJpaTest
class ProductRepositoryIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.datasource.driver-class-name", postgres::getDriverClassName);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");
}
@Autowired
private ProductRepository repository;
@Test
void shouldPersistAndLoadProduct() {
Product saved = repository.save(new Product("Mouse", 5));
assertThat(saved.getId()).isNotNull();
Product loaded = repository.findById(saved.getId()).orElseThrow();
assertThat(loaded.getName()).isEqualTo("Mouse");
assertThat(loaded.getStock()).isEqualTo(5);
}
}Perceba a diferença prática: o teste do repository é mais rápido e mais focado, enquanto o teste web cobre a jornada completa da API. Por outro lado, os dois juntos dão um retrato muito melhor do sistema do que uma suíte enorme de mocks.
Quando usar e quando evitar testes de integração com Testcontainers
Use esse tipo de teste quando houver risco real de integração. Por outro lado, projetos com Spring Data JPA, queries nativas, migrações, múltiplos serviços, contratos de API e regras transacionais se beneficiam muito. Ao mesmo tempo, também faz sentido quando a equipe já sofreu com “funciona na minha máquina” ou com bugs que só aparecem em um banco diferente do H2.
Evite transformar tudo em teste de integração pesado. Por outro lado, não vale a pena subir um container para cada regra simples de cálculo de desconto ou validação de formato. Ao mesmo tempo, essas partes devem continuar em testes unitários rápidos. Na prática, o melhor desenho costuma ser: unitários para regras puras, integração para o que depende de framework, banco, serialização e contratos. Ainda assim, esse equilíbrio evita suíte lenta e ainda entrega confiança real.
Outra decisão técnica importante é o escopo. Por outro lado, se o objetivo é validar o endpoint completo, use @SpringBootTest com porta aleatória. Ao mesmo tempo, se o objetivo é persistência, @DataJpaTest costuma ser mais enxuto. Na prática, se a aplicação tiver várias integrações externas, vale estudar a estratégia para não fazer a suíte depender de serviços remotos. Ainda assim, testcontainers ajuda justamente a manter tudo local e controlado.
Para quem está montando ou revisando uma API, a base do projeto também precisa estar bem organizada. Por outro lado, um guia avançado como API REST Spring Boot Java: Guia Completo com Exemplo Prático ajuda a alinhar estrutura de camadas antes de você elevar a régua dos testes. Ao mesmo tempo, e, se a intenção for entender o contexto maior de qualidade, arquitetura e cobertura, o Guia completo de testes no Spring Boot: evite falhas em produção complementa bem esta discussão.
FAQ sobre testes integracao spring boot testcontainers
Testcontainers substitui H2 nos testes do Spring Boot?
Na maioria dos casos, sim, quando a meta é realismo. Por outro lado, h2 pode servir para cenários simples, mas não reproduz fielmente o comportamento do banco de produção. Ao mesmo tempo, se você usa PostgreSQL ou MySQL em produção, testar com o mesmo banco via Testcontainers reduz surpresas.
Testes de integração com Testcontainers deixam a suíte lenta?
Ficam mais lentos que unitários, mas normalmente ainda são viáveis quando bem organizados. Por outro lado, o segredo é não usar Testcontainers para tudo. Ao mesmo tempo, reserve essa abordagem para fluxos críticos, persistência e endpoints importantes. Na prática, o ganho de confiança costuma compensar o custo.
Posso testar controller, service e repository no mesmo teste?
Pode, e em alguns fluxos críticos isso é até desejável. Por outro lado, só não transforme todos os testes em testes gigantes e difíceis de ler. Ao mesmo tempo, ter um teste de ponta a ponta e alguns testes menores de repository ou service costuma ser um bom equilíbrio.
Conclusão e próximos passos
testes integracao spring boot testcontainers são uma forma prática de sair da ilusão de cobertura e chegar perto do comportamento real da aplicação. Por outro lado, em vez de confiar demais em mocks, você valida a conversa entre controller, service e repository com banco real isolado, do jeito que o sistema realmente vai se comportar quando estiver em uso.
O ganho não é apenas técnico. Por outro lado, o time passa a enxergar falhas de integração antes do deploy, reduz regressões silenciosas e ganha confiança para evoluir schema, queries e endpoints sem medo excessivo. Ao mesmo tempo, se você já tem uma base de API pronta, o próximo passo natural é adaptar um fluxo crítico e transformar esse caminho em teste de integração com banco real. Na prática, comece pequeno, meça o tempo da suíte e amplie aos poucos. Ainda assim, a diferença na qualidade aparece rápido. Por isso, os proximos passos sao validar esse fluxo no seu projeto, ajustar o caso de uso real e cobrir a implementacao com testes.
Testes integracao spring boot testcontainers: referencias externas
Para validar detalhes de implementacao e aprofundar a configuracao, vale consultar a documentacao oficial do Spring Security, o guia de claims no JWT.io e a documentacao do Spring Boot.