Tests against H2 miss database-specific behaviour; Testcontainers makes real dependencies cheap.
In-memory databases behave differently from PostgreSQL or MySQL (SQL dialect, constraints, JSON types, locking), so tests pass and production fails. Testcontainers starts real services in throwaway Docker containers during tests.
Spring Boot integrates it tightly. Add spring-boot-testcontainers and the module you need (org.testcontainers:postgresql), then declare the container as a bean or static field annotated @ServiceConnection. Boot reads the container's host, port and credentials and configures the DataSource (or Kafka, Redis, MongoDB, RabbitMQ connections) automatically; no @DynamicPropertySource boilerplate.
Patterns:
- Annotate a test class with
@Testcontainersand astatic @Container @ServiceConnection PostgreSQLContainer<?>field; the container starts once for the class. - Share containers across test classes by putting
@Bean @ServiceConnectiondefinitions in a@TestConfigurationclass and importing it, so Spring's context cache reuses them. - Combine with
@DataJpaTestand@AutoConfigureTestDatabase(replace = NONE)for fast repository tests against the real database, running your Flyway migrations.
You can also use the same configuration at development time: a TestApplication main in src/test that calls SpringApplication.from(App::main).with(ContainersConfig.class).run(args) starts the app with its containers, replacing hand-maintained docker-compose files. Boot also supports Docker Compose directly with spring-boot-docker-compose.
Example
@TestConfiguration(proxyBeanMethods = false)
class ContainersConfig {
@Bean
@ServiceConnection // Boot wires the DataSource from it
PostgreSQLContainer<?> postgres() {
return new PostgreSQLContainer<>("postgres:17-alpine");
}
}
@SpringBootTest
@Import(ContainersConfig.class)
class BookRepositoryIT {
@Autowired BookRepository repo;
@Test
void findsBooksCaseInsensitively() {
repo.save(new Book("Dune", "Frank Herbert", LocalDate.of(1965, 8, 1)));
assertThat(repo.findByAuthorIgnoreCase("frank herbert")).hasSize(1);
}
}
// src/test/java/.../TestShopApplication.java: run the app locally with containers
class TestShopApplication {
public static void main(String[] args) {
SpringApplication.from(ShopApplication::main).with(ContainersConfig.class).run(args);
}
}Remember
- Testcontainers runs real dependencies in Docker for tests
@ServiceConnectionconfigures connection properties automatically- Put containers in a shared
@TestConfigurationto reuse them - The same setup can run the app locally
Watch out for
- Starting a new container in every test class without sharing, making the suite slow.