Contract tests and Testcontainers: Testing integration realistically
In short: Contract tests check that services stick to agreed interfaces; Testcontainers start real databases and services for tests in Docker.
In more detail: Between unit tests (isolated) and E2E tests (everything together) lie integration tests: do components understand each other? Two tools make them practical.
In Depth
The problem
Mocks lie: whoever replaces the database in a test with a mock does not notice when the real SQL query is faulty or the other team’s API changes. Still, tests should stay fast and repeatable.
Testcontainers
The Testcontainers library (Java, JavaScript, Python, Go, .NET) starts Docker containers for the duration of a test: real PostgreSQL, Redis, Kafka, Elasticsearch.
from testcontainers.postgres import PostgresContainer
with PostgresContainer("postgres:16") as pg:
url = pg.get_connection_url()
# create table, test code against a real DBAdvantages: realistic, each test has a clean environment, no manual set-up; the library handles clean-up. Disadvantages: needs Docker, somewhat slower than mocks. Alternative: docker-compose in CI.
Contract tests
Two services (consumer and provider) agree on a contract: structure of requests and responses. Consumer-driven contracts (for example with Pact): the consumer describes what it needs; the provider checks in its CI that it delivers it. If the provider changes something incompatible, its test fails before it goes into production. OpenAPI specifications can also serve as a contract, with tools that check responses against the schema.
What when?
- Many unit tests for logic.
- Integration tests with Testcontainers for database access, migrations, queues.
- Contract tests between teams/microservices.
- Few E2E tests for main paths. This keeps the test pyramid fast and meaningful. Keep test data small, avoid mutual dependencies between tests and clean up resources reliably.
See also: Test pyramid, Mocking and test doubles, OpenAPI and Swagger, Docker