Overview
Integration tests occupy the middle of the portfolio: your code plus its real dependencies — database, message broker, HTTP peer — but still run from the build, not a deployed environment. Testcontainers changed the economics of this layer: a real Postgres or Kafka in Docker per test run beats an in-memory imitation (H2 is not Postgres) at little extra cost. Spring, Quarkus and Micronaut each ship test support that boots the framework context and, increasingly, wires the containers for you.
Key points
- Test against the real engine: an H2 test passing proves H2 compatibility; dialect quirks, locking behaviour and JSON/array columns only show up on the real database.
- Testcontainers is the centerpiece: programmatic Docker containers with lifecycle tied to the test run, ready-made modules (postgres, kafka, mongodb, redis, localstack, …) and automatic port mapping.
- Container reuse (
testcontainers.reuse.enable=true+.withReuse(true)) keeps a container alive across runs — seconds instead of tens of seconds per local iteration. - Spring slices load a fraction of the context:
@DataJpaTest(JPA layer),@WebMvcTest(controllers + MockMvc),@JsonTest,@RestClientTest; full@SpringBootTestonly when wiring itself is under test. - Quarkus Dev Services:
@QuarkusTestboots the app; unconfigured datasources/brokers are auto-provisioned as Testcontainers behind the scenes — zero test config. - Micronaut:
@MicronautTeststarts the context (fast, thanks to compile-time DI) and supports@MockBeanreplacements and Test Resources for container provisioning. - WireMock stubs external HTTP: verify your client code against a real HTTP server with canned responses, faults and delays — never mock your own HTTP client classes.
- Migrations run in tests: apply Flyway/Liquibase against the container so the schema under test is the schema in production.
Details
Testcontainers patterns
- Per-class static container with
@Testcontainers/@Containeron astaticfield: one container for all tests in the class — the usual default. - Shared singleton container: a base class holding a manually started
staticcontainer reused across the whole module; fastest, needs data isolation discipline. - Per-test containers only when tests mutate global container state (topics, users).
- Spring Boot 3.1+:
@ServiceConnectionon the container field replaces manual@DynamicPropertySourceURL/credentials plumbing for supported types.
Framework test support side by side
| Concern | Spring Boot | Quarkus | Micronaut |
|---|---|---|---|
| Boot the app for a test | @SpringBootTest |
@QuarkusTest |
@MicronautTest |
| Sliced/partial context | @DataJpaTest, @WebMvcTest, @JsonTest |
— (context is cheap; profiles/@TestProfile) |
context is cheap; @Property overrides |
| HTTP assertions | MockMvc (servlet), WebTestClient (reactive/real port) |
REST Assured (bundled convention) | HttpClient injected against the embedded server |
| Replace a bean with a mock | @MockitoBean (formerly @MockBean) |
@InjectMock |
@MockBean |
| Auto containers | Testcontainers + @ServiceConnection |
Dev Services (implicit) | Test Resources |
All three run on JUnit 5 Jupiter extensions, so @Nested, @ParameterizedTest and
custom extensions work identically inside framework tests.
Database state between tests
- Transaction rollback: wrap each test in a transaction rolled back afterwards
(Spring's default in
@DataJpaTest). Fast, but hides commit-time behaviour (constraint deferral, triggers,REQUIRES_NEW), and useless for code that manages its own transactions. - Truncate between tests: slower but honest — the code commits for real. Truncate
tables (or use a tool/extension) in
@BeforeEach; combine with the singleton container. - Never rely on test ordering to share state; each test owns its fixtures.
Examples
@Testcontainers
@SpringBootTest
class OrderRepositoryIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired OrderRepository orders;
@Test
void persistsAndReloadsOrder() {
var saved = orders.save(Order.of("o-1", Money.eur(42)));
assertThat(orders.findById(saved.id())).hasValueSatisfying(
o -> assertThat(o.total()).isEqualTo(Money.eur(42)));
}
}
Flyway migrations on the classpath run automatically on context start, so the container's schema matches production before the first test executes.
Related
- Testing in Java — strategies & tooling map — parent map of the strategy portfolio.
- End-to-end testing — the next boundary out: the deployed system.
- Spring — test slices and Spring Test in framework context.
- Quarkus —
@QuarkusTestand Dev Services in depth. - Micronaut —
@MicronautTestand Test Resources. - Relational databases — the JDBC/JPA layer these tests exercise for real.