Integration tests with Testcontainers
At the top of the pyramid are integration tests — the few tests that boot the whole application and
check that the layers actually work together, ideally against a real database. Spring gives you
@SpringBootTest for the full boot, and Testcontainers for a real database in a throwaway container.
These are the slowest, most realistic tests; you write few of them, for the critical paths. This lesson
covers both, closing the testing module.
@SpringBootTest: boot the whole application
@SpringBootTest starts the entire application context — every bean, real wiring across all layers,
just like production (minus the network, unless you ask for a real port). Verified by running: a
@SpringBootTest context-load test for Dakiya started the full application successfully in about a second —
which alone is a useful test (it proves the beans all wire together and the context is valid; a broken
configuration fails here).
@SpringBootTest
class DakiyaApplicationTests {
@Test
void contextLoads() { } // fails if the application context cannot be built
}
For a full HTTP integration test, boot with a real port and use a client:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ParcelApiIntegrationTest {
@Autowired TestRestTemplate rest; // or MockMvc / WebTestClient
@Test
void booking_then_fetching_a_parcel_worksEndToEnd() {
// POST to create, then GET it back — through the real controller, service, repository, database
}
}
This exercises the whole flow — controller → service → repository → database — with nothing mocked. That
realism is the value, and the slowness (a full boot per test class) is the cost. So use @SpringBootTest
for the handful of critical end-to-end journeys (book a parcel and read it back; the auth flow), not for
everything — the pyramid says most tests are faster unit and slice tests.
The database problem, and Testcontainers
A full integration test wants a real database — but you should test against the database you deploy on (PostgreSQL), not H2, because they differ (the postgres and data-layer lessons). Running a real Postgres for tests used to be painful (install it, keep it clean, shared state between test runs). Testcontainers solves this: it starts a real PostgreSQL in a throwaway Docker container for the test, and tears it down after — a genuine Postgres, isolated, requiring only Docker:
@SpringBootTest
@Testcontainers
class ParcelApiIntegrationTest {
@Container
@ServiceConnection // Spring Boot auto-wires the datasource to this container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
// tests run against a real Postgres 16, started for this test and thrown away after
}
@ServiceConnection (Spring Boot 3.1+) is the pleasant part: Boot detects the container and points the
application's datasource at it automatically — no manual URL wiring. Now your integration tests run against
the actual database engine you use in production, catching the differences H2 would hide (Postgres-specific
SQL, exact constraint behaviour, real types). Testcontainers requires Docker to be available (locally
and in CI), which is its one prerequisite.
What integration tests are for — and the cost
Use @SpringBootTest (with Testcontainers for the DB) for:
- Critical end-to-end flows — the journeys that must never break (create and retrieve a parcel; the security flow), tested through every real layer.
- Cross-layer wiring — that the controller, service, repository and database actually cooperate, not just each in isolation.
- Database fidelity — behaviour that depends on the real Postgres, where H2 would mislead.
But respect the cost: a full boot per class is seconds, and a suite of hundreds of @SpringBootTest
classes is slow enough that developers stop running it. So keep integration tests few and focused — the
critical paths only — and put the bulk of your coverage in fast unit tests (logic) and slice tests
(@WebMvcTest, @DataJpaTest). This is the pyramid enforced: many fast tests at the base, a few slow
realistic ones at the top.
The complete testing picture for Dakiya
Pulling the whole module together, a well-tested Spring service has:
- Unit tests (many) — service/business logic, mocked dependencies, no Spring. Milliseconds.
@WebMvcTest(some) — controller behaviour: routing, status, validation, error handling, security.@DataJpaTest(some) — repository queries and mappings, against a database.@SpringBootTest+ Testcontainers (few) — critical end-to-end flows against a real Postgres.
Weighted towards the failure and risky paths, testing your logic not the framework, and shaped like a
pyramid — mostly fast, a few slow. Verified in this module: unit tests of the service pass, a @WebMvcTest
of the controller passes (404/400/401), a @DataJpaTest of the repository passes, and the @SpringBootTest
context boots. That combination — fast focused tests plus a few realistic ones — is what lets you change a
Spring app confidently, which (with merging deploying to real users) is exactly the safety net you need.
Check your work
@SpringBootTest. Boots the whole context (all layers, real wiring) — a context-load test alone proves
the beans wire (verified: Dakiya's booted). With RANDOM_PORT + a client, tests full HTTP flows end to end,
nothing mocked. Slow — for critical journeys only.
Testcontainers. Starts a real Postgres in a throwaway Docker container for the test; @ServiceConnection
auto-wires the datasource. Tests against the actual production database engine, catching what H2 hides.
Requires Docker.
What integration tests are for. Critical end-to-end flows, cross-layer wiring, and database-fidelity cases — few and focused, because a full boot costs seconds.
The whole pyramid. Many unit tests (logic) → some slice tests (@WebMvcTest/@DataJpaTest) → few
integration tests (@SpringBootTest + Testcontainers) — verified passing in this module; weighted to
failures/risky paths; testing your logic, not Spring.
Practice
- Write a
@SpringBootTestcontextLoadstest and confirm it boots the app (reproduce the verified pass); break a bean's wiring and watch it fail. - Write a
@SpringBootTest(webEnvironment = RANDOM_PORT)test that POSTs a parcel then GETs it back through the real stack. - Add Testcontainers with a
PostgreSQLContainerand@ServiceConnection; run an integration test against real Postgres and confirm the datasource wired automatically. (Requires Docker.) - Find a query that behaves differently on H2 vs Postgres and confirm the Testcontainers test catches it where an H2 test would not.
- Time a
@SpringBootTestclass vs a unit test class and reflect on why the suite must be mostly the fast kind. - Lay out Dakiya's full test suite across the four types and check the shape is bottom-heavy.
Official documentation
- Spring Boot — @SpringBootTest — Full-context integration tests.
- Spring Boot — Testcontainers — Real databases in tests,
@ServiceConnection. - Testcontainers for Java — Throwaway containers for tests.
Next: layering — controller, service, repository, and where logic lives.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship