PushBackLog

Contract Testing

Advisory enforcement Complete by PushBackLog team
Topic: testing Topic: distributed-systems Skillset: backend Technology: generic Stage: execution Stage: review

Contract Testing

Status: Complete
Category: Testing
Default enforcement: Advisory
Author: PushBackLog team


Tags

  • Topic: testing, distributed-systems
  • Skillset: backend
  • Technology: generic
  • Stage: execution, review

Summary

Contract testing verifies that a service provider and its consumers agree on the shape of their interface without requiring both to be running simultaneously. Each consumer publishes a “contract” — the requests it makes and the responses it expects — which the provider verifies it can satisfy. Contract testing replaces brittle cross-service E2E tests for integration points, enabling teams to verify compatibility independently and continuously.


Rationale

Cross-service E2E tests are expensive and fragile

Testing the integration between two services by running them both in a shared environment is the obvious approach, but it has serious costs: both services must be deployed and available simultaneously, test data must be shared and managed, and failures can originate from either service or from the shared infrastructure. These tests are slow, flaky, and hard to diagnose when they fail.

Consumer-driven contracts shift the integration risk left

Contract testing inverts the dependency: each consumer defines what it needs from the provider, and the provider verifies it can satisfy those contracts. The verification happens in isolation — no shared environment needed. This means consumers can verify compatibility with the provider’s current state without a joint deployment, and providers can detect which consumers would break before a change is released.

The most widely used contract testing tool is Pact, which implements consumer-driven contracts with a publish-verify workflow via a broker.


Guidance

How it works (Pact workflow)

1. Consumer team writes a consumer test that records interactions
   → Pact saves these as a "contract" (JSON pact file)

2. Contract is published to a Pact Broker

3. Provider CI pipeline pulls the pact and verifies it against the real provider
   → If the provider's response does not match the contract, the build fails

4. "Can I deploy?" check: Pact Broker reports whether consumer and provider
   are compatible before any deployment

Consumer test (TypeScript / Pact)

import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like } = MatchersV3;

const provider = new PactV3({
  consumer: 'OrderService',
  provider: 'PaymentService',
  dir: './pacts',
});

describe('PaymentService contract', () => {
  it('charges a card successfully', async () => {
    await provider
      .given('a valid card token exists')
      .uponReceiving('a charge request')
      .withRequest({
        method: 'POST',
        path: '/payments',
        body: { amount: like(4999), currency: like('GBP'), token: like('tok_abc') },
      })
      .willRespondWith({
        status: 201,
        body: {
          id: like('pay_xyz'),
          status: like('succeeded'),
          amount: like(4999),
        },
      });

    const result = await provider.executeTest(async (mockServer) => {
      const client = new PaymentClient(mockServer.url);
      return client.charge({ amount: 4999, currency: 'GBP', token: 'tok_abc' });
    });

    expect(result.status).toBe('succeeded');
  });
});

Provider verification

// Provider-side verification — runs in PaymentService CI
new Verifier({
  provider: 'PaymentService',
  providerBaseUrl: 'http://localhost:3001',
  pactBrokerUrl: process.env.PACT_BROKER_URL,
  publishVerificationResult: true,
  providerVersion: process.env.GIT_SHA,
}).verifyProvider();

Matchers — what to match vs. what to verify

Contract tests use matchers rather than exact values wherever the exact value is not semantically meaningful:

MatcherUse when
like(value)Type and presence matter, not exact value
eachLike(template)Array of items matching the template
regex(pattern, example)Format matters (UUIDs, ISO dates, etc.)
Exact valueWhen a specific enum value, magic string, or exact code is semantically required

What contract testing does and does not verify

Does verify:

  • The response body shape matches the consumer’s expectations
  • Required fields are present
  • Field types are correct
  • HTTP status codes match

Does not verify:

  • Business logic correctness
  • Performance under load
  • State consistency across multiple calls
  • Security — contract testing is not a substitute for auth testing

When to use contract testing

Contract testing is most valuable when:

  • Multiple teams own different services that integrate via API
  • You want to verify integration cheaply without a shared environment
  • You need to detect when a provider change would break a consumer before deployment

For a monolith or single-team services, the overhead of contract testing may not be justified — integration tests within the same codebase are simpler.