In a monolith, testing is relatively straightforward — one deployment, one database. In microservices you are testing a distributed system. How do you ensure that services work together without running everything at once?
Testing Pyramid¶
Unit tests (base): Business logic, domain objects. Fast, isolated. Integration tests (middle): Service + database, service + message broker. Contract tests: Verifying that the API contract between services holds. End-to-end (top): The entire system. Slow, brittle — keep to a minimum.
Consumer-Driven Contract Testing¶
Service A (consumer) defines what it expects from service B (provider). The contract is verified on both sides independently. Pact framework:
// Consumer test (service A)
@Pact(consumer = "OrderService", provider = "UserService")
public RequestResponsePact userDetailsPact(PactDslWithProvider builder) {
return builder
.given("user 123 exists")
.uponReceiving("get user details")
.path("/api/users/123")
.method("GET")
.willRespondWith()
.status(200)
.body(new PactDslJsonBody()
.integerType("id", 123)
.stringType("name", "Jan Novák"))
.toPact();
}
Integration tests with Testcontainers¶
Testcontainers runs Docker containers in tests — real PostgreSQL, Redis, RabbitMQ. No database mocks, no in-memory substitutes. Slower, but you verify real behavior.
E2E: minimal, but necessary¶
Happy path scenarios — user logs in, creates an order, pays. Smoke tests after deployment. Do not cover edge cases with E2E tests — that is the job of unit and contract tests.
Test pyramid, not test ice cream¶
Many unit tests, fewer integration tests, minimum E2E. Contract tests are the key innovation for microservices — they verify compatibility without running the entire system.
Need help with implementation?
Our experts can help with design, implementation, and operations. From architecture to production.
Contact us