Pact vs Integration Tests: When to Use Contract Testing

Pact vs Integration Tests: When to Use Contract Testing

"Should we use Pact or integration tests?" is a question that comes up whenever a team starts decomposing a monolith into services. The honest answer is: it depends on your team structure, deployment cadence, and tolerance for the complexity each approach introduces.

This article explains both approaches clearly, compares them on the dimensions that actually matter, and gives you a decision framework for choosing — or combining — them.

What Integration Tests Are

An integration test verifies that two or more real components work together correctly. In a microservices context, this typically means:

  1. Deploy the consumer service to a test environment
  2. Deploy the provider service to the same environment (or a shared staging environment)
  3. Run tests against the consumer that make real HTTP calls to the real provider

This is straightforward conceptually. You're testing the actual integration. If it works in test, it works in production (modulo environment differences).

// integration test
it('should fetch user profile from user-service', async () => {
  // Uses real user-service running at test URL
  const response = await fetch('http://user-service.test/users/1');
  const user = await response.json();
  
  expect(user.id).toBe(1);
  expect(user.email).toBeDefined();
});

What Contract Tests Are

A contract test verifies that a consumer and provider agree on an API interface — without both services needing to run simultaneously.

The consumer defines its expectations (the contract). The provider verifies it can meet those expectations. The two tests run independently, in separate pipelines.

// Pact consumer test — no real provider needed
provider
  .given('user 1 exists')
  .uponReceiving('GET /users/1')
  .willRespondWith({ status: 200, body: { id: integer(1), email: string('user@example.com') } });

return provider.executeTest(async (mockProvider) => {
  const response = await fetch(`${mockProvider.url}/users/1`);
  const user = await response.json();
  expect(user.id).toBe(1);
});

The Comparison

Coupling between teams

Integration tests: Both services must be deployed simultaneously. If the user-service is broken or unavailable, the frontend integration tests fail — even if the frontend code is fine. This creates coupling.

Contract tests: Each team runs their tests independently. Consumer tests don't need the real provider. Provider verification runs without the consumer. Teams can deploy independently, as long as contracts pass.

Winner for independent teams: Contract tests

What failures mean

Integration tests: A failure means something in the actual integration is broken. It could be the consumer, the provider, the network, the test data, or the environment. Debugging takes time.

Contract tests: A consumer failure means the consumer code doesn't make the request it expects to. A provider verification failure means the provider can't fulfill a consumer's documented expectation. The failures are specific.

Winner for clear failure attribution: Contract tests

Coverage of real behavior

Integration tests: Test the actual combination of real code. If the provider has a bug that's masked by the consumer working around it, an integration test catches it. If a provider returns data in an unexpected format that happens to work, you'll see it.

Contract tests: Test only what the consumer explicitly defined. If the provider has a bug the consumer doesn't exercise (because it doesn't use that field), contract tests don't catch it. Contract tests don't catch environment-specific issues (wrong database configuration, missing env vars).

Winner for catching real bugs: Integration tests

Setup and maintenance cost

Integration tests: Require a shared test environment, test data seeding strategy, deployed services, and coordination between teams when environments are unstable.

Contract tests: Require buy-in from both teams, state handler maintenance, a pact broker if you want the full workflow, and understanding of the Pact framework.

Both have real costs. Neither is obviously simpler to set up from scratch.

Speed

Integration tests: Slow. Network calls, database queries, service startup times. Shared environment queuing.

Contract tests: Fast. Consumer tests run against a local mock. Provider verification runs a real server but against a fixed set of interactions.

Winner for speed: Contract tests

Confidence in production readiness

Integration tests: High confidence. If your test environment is representative of production, passing integration tests mean the integration works.

Contract tests: Lower confidence by themselves. They verify the interface agreement, not the full behavior. You still need some end-to-end verification.

Winner for production confidence: Integration tests (though the gap narrows with good contract coverage)

What Each Approach Catches

Integration tests catch:

  • Wrong URL structure (consumer calls /user/1, provider only handles /users/1)
  • Wrong authentication headers
  • Wrong request body format
  • Provider behavior bugs that affect the consumer
  • Environment-specific failures
  • Data not present in test environment
  • Timeout and performance issues

Contract tests catch:

  • Breaking API changes (provider removes a field the consumer uses)
  • Response format changes (provider changes id from integer to string)
  • Status code changes (provider changes 404 to 204 for missing resources)
  • Header changes (provider removes a content-type header the consumer expects)
  • Incompatible new requirements (consumer adds a new interaction the provider doesn't support)

What neither catches reliably:

  • Business logic bugs within a single service
  • End-to-end user journey correctness
  • Performance at scale
  • Real production data edge cases

The Team Structure Factor

This is the key variable most articles skip over.

Same team owns consumer and provider: Integration tests are usually simpler. The team coordinates directly, deploys together, and can debug integration issues quickly. The overhead of a full Pact workflow (broker, can-i-deploy, state handlers) may not be worth it.

Different teams own consumer and provider: Contract testing shines. Each team needs to know: "Can I deploy this version without breaking other services?" Integration tests require coordination; contract tests make the agreement explicit and automatable.

Multiple consumers of one provider: Contract testing is nearly mandatory. Each consumer defines what it needs, and the provider verifies all of them. Without this, the provider team is guessing what consumers actually use — and they'll break things they thought were safe to change.

Migration Path

Most teams start with integration tests and move toward contract testing as services and teams grow. A common sequence:

Phase 1: Everything in a monolith, no contract testing needed.

Phase 2: First service extraction. Integration tests against a shared staging environment. Works fine for small teams.

Phase 3: Multiple teams, multiple consumers of the same service. Staging environment becomes a bottleneck. Deployment coupling becomes painful. This is when Pact starts paying off.

Phase 4: Contract tests for stable, high-traffic API surfaces. Integration tests for complex flows (like checkout) where the full end-to-end behavior matters more than the interface. Both coexist.

Practical Decision Framework

Use integration tests when:

  • One team owns both consumer and provider
  • The API is internal and rarely changes
  • You need confidence in actual runtime behavior
  • Environment setup is simple
  • Your test suite is small enough that speed isn't a constraint

Use contract tests when:

  • Consumer and provider are owned by different teams
  • Multiple services consume the same API
  • Deployment independence matters (you want to deploy services separately without coordinating)
  • Integration test environments are flaky, slow, or hard to maintain
  • You're providing a public or external API

Use both when:

  • You have high-value user journeys that require end-to-end confidence
  • You want fast feedback from contract tests plus production confidence from end-to-end tests
  • Your architecture has both stable internal APIs (contract test them) and complex integrations (integration test them)

The Practical Reality

Most teams are better served by starting simpler. If you're a two-person team with one monolith and one mobile client, don't introduce Pact. If you have ten microservices across four teams and deployment is painful, Pact will reduce that pain significantly.

The technology is straightforward. The harder part is the organizational discipline: consumer teams must keep their pacts up to date, provider teams must run verification in CI, and everyone must respect can-i-deploy results as a deployment gate. Without that discipline, the pact files become stale and lose value.

Contract testing is an organizational contract as much as a technical one.

Read more