RizTech Academy logo
RizTech Academy
TestingLesson 5 of 540 min

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

  1. Write a @SpringBootTest contextLoads test and confirm it boots the app (reproduce the verified pass); break a bean's wiring and watch it fail.
  2. Write a @SpringBootTest(webEnvironment = RANDOM_PORT) test that POSTs a parcel then GETs it back through the real stack.
  3. Add Testcontainers with a PostgreSQLContainer and @ServiceConnection; run an integration test against real Postgres and confirm the datasource wired automatically. (Requires Docker.)
  4. Find a query that behaves differently on H2 vs Postgres and confirm the Testcontainers test catches it where an H2 test would not.
  5. Time a @SpringBootTest class vs a unit test class and reflect on why the suite must be mostly the fast kind.
  6. Lay out Dakiya's full test suite across the four types and check the shape is bottom-heavy.

Official documentation

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