Contract Testing (Pact)

23Contract Testing (Pact)

What you will master here

  • What contract testing is and the problem it solves
  • Consumer-driven contracts (CDC)
  • How Pact works: consumer test → pact file → broker → producer verification
  • Writing consumer tests (JS, Java)
  • Provider verification
  • Pact Broker
  • Comparison vs traditional integration tests

23.1 The problem

Microservice A calls Microservice B. Traditional ways to test the integration:

Contract testing: the consumer declares "I send X, expect Y back"; the producer verifies it can satisfy that contract. Producer changes that break the contract fail BEFORE deploy.

23.2 How Pact works

Consumer test runs against mock Pact file (JSON) expected interactions Pact Broker store + share Provider verify replay against real service Consumer DRIVES the contract — provider's CI fails if it can't satisfy.
Figure — Consumer-Driven Contract flow.

23.3 Consumer test (Node)

// users-client.consumer.spec.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, integer, string } = MatchersV3;

const provider = new PactV3({ consumer: 'web', provider: 'users-api' });

test('GET /users/42', async () => {
  provider
    .given('user 42 exists')
    .uponReceiving('a request for user 42')
    .withRequest({ method: 'GET', path: '/users/42',
                   headers: { Accept: 'application/json' } })
    .willRespondWith({
      status: 200,
      headers: { 'Content-Type': 'application/json' },
      body: like({
        id: integer(42),
        email: string('alice@test.com'),
        orders: eachLike({ id: string('o_1'), total: integer(99) }),
      }),
    });

  await provider.executeTest(async (mockServer) => {
    const client = new UsersClient(mockServer.url);
    const user = await client.getUser(42);
    expect(user.email).toBe('alice@test.com');
  });
});

Result: a JSON pact file in pacts/web-users-api.json describing the interaction.

23.4 Publishing to the broker

npm i -D @pact-foundation/pact-node
pact-broker publish ./pacts \
  --consumer-app-version=$GIT_SHA \
  --branch=$GIT_BRANCH \
  --broker-base-url=https://pact-broker.acme.com

23.5 Provider verification (Java)

@Provider("users-api")
@PactBroker(url = "https://pact-broker.acme.com")
public class UsersApiPactTest {
  @LocalServerPort int port;

  @BeforeEach
  void before(PactVerificationContext ctx) {
    ctx.setTarget(new HttpTestTarget("localhost", port));
  }

  @State("user 42 exists")
  void userExists() {
    /* seed DB so the request will succeed */
    userRepo.save(new User(42, "alice@test.com"));
  }

  @TestTemplate
  @ExtendWith(PactVerificationInvocationContextProvider.class)
  void pactVerificationTestTemplate(PactVerificationContext context) {
    context.verifyInteraction();
  }
}

23.6 Pact Broker

Central service for pact storage and orchestration. Key features:

# Gate deploy
pact-broker can-i-deploy --pacticipant web --version $GIT_SHA --to-environment production

23.7 Matchers (the magic)

You don't pin exact values — you specify shapes. Producer can return different ACTUAL values; only the shape must match.

MatcherMeans
like(example)Same type as example
eachLike([example])Array of elements matching example
integer(42)Any integer
string('x')Any string
regex(/pattern/, example)String matching regex
timestamp('yyyy-MM-dd', example)Date format

23.8 Contract vs Schema vs E2E

ApproachCatchesMisses
OpenAPI schema checkType/structure changesSemantic mismatch (right shape, wrong meaning)
Contract testing (Pact)Real consumer/producer mismatch with actual examplesBugs entirely within one service
End-to-endFull system behaviourSlow, flaky, expensive to maintain at scale
Real-world setupUse ALL three layered: schema for cheapest checks, contracts to gate cross-service changes, a small E2E suite for critical user journeys. Contract testing replaces "many integration tests" not "all tests".

Module 49 — Pact Q&A

What problem does contract testing solve?
In a microservice architecture, producer changes can silently break consumers. End-to-end tests catch it but slowly. Contract testing catches it BEFORE deploy — consumer test pins the expectation, producer's CI verifies it.
What does "consumer-driven" mean?
The consumer (caller) writes the contract describing what it expects. The producer (callee) must satisfy it. Reverse of schema-first APIs where the producer defines first. Better aligns to actual consumer needs.
What is a Pact file?
JSON document describing one or more interactions: request shape, expected response shape, and provider states needed to satisfy them. Generated by the consumer test. Published to a broker.
What is the Pact Broker?
Central service that stores pacts, links consumer + provider versions, triggers verification webhooks, and answers "can I deploy?" via the can-i-deploy command. The brain of a Pact ecosystem.
What is can-i-deploy?
A broker query: "given this version of service X, is it safe to deploy to environment Y based on the latest verified pacts?" Returns yes/no. Wired into deploy pipelines as a hard gate.
What's a provider state?
A named precondition the producer must set up before replaying an interaction — e.g. "user 42 exists". The provider verification test registers a function for each state that seeds the data, then runs the interaction.
Why use matchers like like instead of pinning exact values?
You don't care about specific values, only shape/type. A consumer that pinned id: 42 would break when producer returns id 43 — pointless flakes. Match shape, allow values to vary.
Contract vs E2E — when each?
Contract for cross-service expectations — fast, cheap, runs in each service's CI independently. E2E for true user journeys through multiple services. Contract dramatically reduces the number of E2Es needed.
Pact vs Spring Cloud Contract?
Pact: consumer-driven, language-agnostic broker, mature ecosystem. Spring Cloud Contract: producer-driven (provider writes the contract), Spring-native, can also generate WireMock stubs for consumers. Pick by team conventions.
Does Pact support async/message queues?
Yes — Pact Message supports Kafka, RabbitMQ, etc. Consumer expectation is "given message X published, my handler does Y". Producer verifies it can produce a message matching that shape.