Handbooks / Microservices / Chapter 9
Testing Microservices
7 pages · ~5 min✓ Reviewed
Builds on Observability and Deployment. Next up: Teams, Ownership and CI/CD.
Service Test Pyramid Mental Model
1. The Pyramid
- 1E2Every few, full system
- 2Component / integrationfewer, one service
- 3Unitmany, fast, isolated
| Going up | Effect |
|---|---|
| Speed | slower |
| Determinism | flakier |
| Debugging | costlier |
| Realism | closer to production |
2. Reshaping for Microservices
- Services add a contract layer between them
- Little internal logic: the pyramid becomes a honeycomb
- A honeycomb is heavy on integration tests
3. Placement Rule
4. Mistakes
- Ice-cream cone: mostly manual and E2E tests
- Few unit tests, so feedback is slow
- Suites turn flaky
Each layer up buys realism and pays in speed and trust. Catch bugs low.
Unit and Component Tests
1. Unit vs Component
- Unit test: one class or function
- No network or disk
- Runs in milliseconds
- Solitary: collaborators mocked; sociable: real ones
- Component test: one whole service through its public API (HTTP or messages)
- External dependencies replaced by in-process fakes or stubs
2. Test Doubles
| Double | Does | Example |
|---|---|---|
| Stub | returns canned answers | fixed user JSON |
| Mock | verifies interactions | verify(client).send() |
| Fake | working lightweight version | in-memory repository |
3. Example: Order Service
- App starts in-process:
MockMvcor FastAPITestClient GET /orders/7calls/users/1downstream- WireMock stubs that call; the client base URL points at WireMock
Consumer-Driven Contract Tests with Pact
1. Contract Flow
- Contract test: consumer expectations and provider behaviour agree, with no joint deploy
- Consumer: test a Pact mock provider; request and response go to a pact JSON
- Provider: Pact replays recorded requests on the real service and checks responses
- Consumer testmock provider
- Pact fileJSON
- Brokerpublish
- Provider verifyreal replay
2. Provider States and Broker
- Provider state
given 'user 1 exists'seeds the data before replay - Pact Broker / PactFlow store pacts and verification results
- can-i-deploy asks: is this version safe against its partners?
pact-broker can-i-deploy \ --pacticipant web-app \ --version 1.4.0 \ --to-environment production
gate a release on verified pacts
3. Mistakes
- Asserting every field, not just what the consumer uses
- Exact values instead of matchers:
like(),eachLike(), regex - Treating Pact as functional testing of the provider
Integration Tests with Testcontainers
1. What and Why
- Testcontainers starts real throwaway Docker containers from test code
- Postgres, Kafka, Redis, LocalStack; the Ryuk reaper removes them afterwards
- Real dependency catches SQL dialect, migration, serialization and driver bugs
- In-memory fakes like H2 miss these
2. Java Example
- Start the container, then wire
pg.getJdbcUrl()into the datasource
@Container static PostgreSQLContainer<?> pg =
new PostgreSQLContainer<>("postgres:16");
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", pg::getJdbcUrl);
}static = one container for the whole class
3. Speed and Mistakes
| Speed tip | How |
|---|---|
| Share containers | static or singleton across test classes |
| Local reuse | reuse mode |
| Stable images | pin the tag |
| Clean data | truncate or transaction per test |
End-to-End Tests and Testing in Production
1. What E2E Tests Are and Cost
- E2E test drives the fully deployed system via its real entry point (UI or public API)
- It crosses all services and all datastores
| Cost | Why |
|---|---|
| Slow | Whole system, real network and data |
| Flaky | Timing and shared environments |
| Hard to debug | Which service failed? |
| Bottleneck | One shared staging environment |
2. Keep Them Few
- A handful of critical journeys: login, checkout
- Run them as smoke tests after deploy
- Not for every edge case; push those to lower layers
3. Testing in Production
Expose to few users
Service Testing Cheat Sheet
1. Layers
- Unitms, isolated
- Componentstubbed deps
- ContractPact
- IntegrationTestcontainers
- E2Efew journeys
- Put each check at the lowest layer that can catch the bug
- E2E only for critical journeys, such as login and checkout
2. Pact and Testcontainers
- 1Consumer testwrites pact
- 2Brokerpublish pact
- 3Providerverifies
- 4can-i-deploygates release
- Real dependency in Docker, started from test code
- Pin tags, never
latest - Use the mapped port and a wait strategy, not sleeps
- Share containers across test classes
@Container static PostgreSQLContainer<?> pg =
new PostgreSQLContainer<>("postgres:16");pinned tag
3. Production safety net
- Feature flags and canary limit blast radius
- Synthetic probes check live journeys
- Metrics and alerts, plus fast rollback
Check yourself
A provider renames the response field email to mail. The consumer's Pact test only asserts on name. Does provider verification fail?
- No, it passes.
- The pact holds only fields the consumer asserts on, so verification replays those.
- A rename of an unread field is not caught.
A Testcontainers test uses port 5432 and Thread.sleep(5000). It fails randomly on CI. Why?
- Docker maps a random host port; use
getMappedPort()orgetJdbcUrl(). - A sleep is a guess; a slow start fails it. Use a wait strategy.
An E2E checkout test fails because tax rounds wrongly in one service. Where should the fix be tested?
- Add a unit test on the tax calculation, the lowest layer that catches it.
- Keep the E2E test as the signal.