|$ curl https://forge-ai.dev/api/markdown?path=docs/testing/contract
$cat docs/contract-testing.md
updated Recently·30 min read·published

Contract Testing

TestingIntermediate🎯Free Tools
Introduction

Contract testing verifies that API consumers and providers agree on the shape of data exchanged between them. Instead of end-to-end integration tests (slow, fragile), contract tests define expectations (contracts) that each side verifies independently.

Consumer-driven contracts (CDC) flip the traditional approach: the consumer defines what it expects from the API, and the provider must satisfy those expectations. This catches breaking changes before they reach production.

Pact Setup
install.sh
Bash
1# Install Pact
2npm install --save-dev @pact-foundation/pact
3
4# For Node.js providers
5npm install --save-dev @pact-foundation/pact-node
consumer.test.ts
TypeScript
1// Consumer test: define what the frontend expects from the API
2import { PactV4, MatchersV3 } from "@pact-foundation/pact";
3import path from "path";
4
5const provider = new PactV4({
6 consumer: "WebFrontend",
7 provider: "UserAPI",
8 dir: path.resolve(process.cwd(), "pacts"),
9 logLevel: "warn",
10});
11
12describe("User API Consumer", () => {
13 it("returns a user by ID", async () => {
14 await provider
15 .addInteraction()
16 .given("user 123 exists")
17 .uponReceiving("a request for user 123")
18 .withRequest("GET", "/api/users/123")
19 .willRespondWith(200, (builder) => {
20 builder.headers({ "Content-Type": "application/json" });
21 builder.jsonBody({
22 id: MatchersV3.integer(123),
23 name: MatchersV3.string("Alice"),
24 email: MatchersV3.email(),
25 createdAt: MatchersV3.isoDateTime(),
26 });
27 })
28 .executeTest(async (mockServer) => {
29 // Test your actual API client against the mock
30 const user = await fetchUser(mockServer.url, "123");
31 expect(user.name).toBe("Alice");
32 expect(user.email).toMatch(/@/);
33 });
34 });
35});
36
37// Your actual API client (the code being tested)
38async function fetchUser(baseUrl: string, id: string) {
39 const res = await fetch(`${baseUrl}/api/users/${id}`);
40 if (!res.ok) throw new Error("User not found");
41 return res.json();
42}

info

The consumer test generates a pact file (JSON contract). This file is shared with the provider team, either via a Pact Broker or as a CI artifact.
Provider Verification
provider.test.ts
TypeScript
1// Provider test: verify the API satisfies the contract
2import { Verifier } from "@pact-foundation/pact";
3
4describe("User API Provider", () => {
5 it("validates the expectations of WebFrontend", async () => {
6 const server = await startUserAPI(3001);
7
8 await new Verifier({
9 provider: "UserAPI",
10 providerBaseUrl: "http://localhost:3001",
11 pactUrls: [
12 path.resolve(process.cwd(), "pacts", "webfrontend-userapi.json"),
13 ],
14 stateHandlers: {
15 "user 123 exists": async () => {
16 // Set up test data for this state
17 await seedUser({ id: 123, name: "Alice", email: "alice@example.com" });
18 },
19 },
20 }).verifyProvider();
21
22 await server.close();
23 });
24});

best practice

Use stateHandlers to set up realistic test data for each interaction state. This ensures the provider can actually serve the data the consumer expects.
Pact Broker
pact-broker.yml
YAML
1# Docker Compose for Pact Broker
2version: '3.8'
3services:
4 postgres:
5 image: postgres:16
6 environment:
7 POSTGRES_USER: pact
8 POSTGRES_PASSWORD: pact
9 POSTGRES_DB: pact_broker
10 volumes:
11 - pact-data:/var/lib/postgresql/data
12
13 pact-broker:
14 image: pactfoundation/pact-broker:latest
15 ports:
16 - "9292:9292"
17 environment:
18 PACT_BROKER_DATABASE_URL: postgres://pact:pact@postgres/pact_broker
19 PACT_BROKER_BASIC_AUTH_USERNAME: admin
20 PACT_BROKER_BASIC_AUTH_PASSWORD: admin
21 PACT_BROKER_ALLOW_PUBLIC_READ: "true"
22 depends_on:
23 - postgres
24
25volumes:
26 pact-data:
27
28# Publish pacts:
29# PACT_BROKER_BASE_URL=http://localhost:9292 npx pact publish
30
31# Verify with broker:
32# PACT_BROKER_BASE_URL=http://localhost:9292 npx pact verify
$Blueprint — Engineering Documentation·Section ID: TEST-CT-01·Revision: 1.0