Contract Testing with Pact and Alternatives

Contract testing verifies that two services agree on the shape and semantics of the messages they exchange, without spinning up both services together.

Contract Testing: Catching Integration Bugs Without a Fragile End-to-End Suite

Contract testing sits between unit tests (fast, but only prove one component works in isolation) and end-to-end tests (prove the whole system works, but slow and flaky). A consumer service declares what it expects from a provider (fields, types, values), and the provider verifies against that expectation on every build. When the provider changes something that breaks a consumer's expectation, the provider's build fails — before anything is deployed. It's especially valuable in microservice architectures where end-to-end suites become unmaintainable and where teams deploy independently.

Consumer-driven vs. provider-driven

Consumer-driven contracts (CDC), the model popularized by Pact, put the consumer in charge: consumers write tests that record their expectations, publish the contracts to a broker, and providers verify against them. This ensures providers know exactly who depends on what and how — no over-serving. Provider-driven contracts (used with OpenAPI, Protobuf schemas) go the other way: the provider publishes its interface, consumers verify they conform. Simpler to adopt, less precise (consumers may claim to use fields they don't). Most orgs mix: OpenAPI/Protobuf for the public shape, Pact for the actual paths consumers exercise.

The Pact workflow, concretely

(1) Consumer test: in the consumer's test suite, describe an interaction ('GET /users/123 returns a user with id, name, email') and write it against a mock provider. The Pact library records the interaction as a JSON contract file. (2) Publish: the CI job pushes the contract to a Pact Broker (self-hosted or Pactflow SaaS), tagged with the consumer's branch and git SHA. (3) Provider verification: the provider's CI pulls the latest contracts from the broker, runs each interaction against the real provider code, and reports pass/fail back to the broker. (4) Deployment gate: 'can-i-deploy' CLI checks whether the version you're about to deploy is compatible with all consumer versions currently in production.

Adoption without over-committing

Start with one high-value integration (usually two services owned by two teams that break each other often). Prove the workflow end-to-end: consumer test → broker → provider verification → can-i-deploy gating a deploy. Only after that flywheel spins expand to other integrations. Don't try to contract-test every interaction — internal-only helper services and read-only reference data usually aren't worth the ceremony. Target: the interactions between teams, especially where a bug would cause a production incident that took hours to diagnose.

Contract testing does not replace all E2E

Contract tests prove that two services agree on message shape and content for the interactions the consumer exercises. They don't prove: end-to-end business flows across 5+ services, UI-level rendering, latency and timeout behavior under load, race conditions. Keep a small suite of E2E tests for the highest-value user journeys (sign-up, checkout, primary daily action) and let contract tests carry the load for cross-service correctness. Typical mature setup: 70% unit, 20% contract, 8% integration, 2% E2E — flipped from the classic Selenium-heavy era.

Common pitfalls

(1) Contracts too loose (matchers that accept any string) — you're testing shape but not values, so real bugs pass. Use strict matchers where semantics matter. (2) Contracts too tight (asserting exact JSON strings) — provider adds a harmless optional field and consumer's contract fails. Use structural matchers with 'like' and 'each-like'. (3) Consumers never run their tests, so contracts go stale. Enforce contract-publishing on every consumer CI run, not just nightly. (4) Provider verifications skipped when 'urgent' hotfixes go out — undermines the whole gate. If a hotfix bypasses verification, an incident review should follow.

Frequently asked questions

GraphQL — do we still need contract testing?
Yes. The schema tells you what's possible; contract tests tell you what consumers actually use and rely on. GraphQL Federation makes this especially important since subgraph changes can silently break consumers of specific fields.
Async/event-driven services — does Pact work?
Yes. Pact supports message contracts (Kafka, RabbitMQ, SQS). Consumer records the expected message shape; provider verifies its emitter produces matching messages. Adoption is somewhat behind HTTP contract testing but growing.
How does this compare to using OpenAPI as a contract?
OpenAPI is a provider-declared spec. Consumer-driven contracts prove which parts of that spec are actually consumed and how. Best practice: use OpenAPI for the surface, Pact (or equivalent) for the actual dependency graph. They're complementary.

Related fundraising guides (40)

Investor directory · Fundraising library · Articles A–Z · Company funding database