|$ curl https://forge-ai.dev/api/markdown?path=docs/testing/contract
$cat docs/contract-testing.md
updated Recently·30 min read·published
Contract Testing
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 |
| 2 | npm install --save-dev @pact-foundation/pact |
| 3 | |
| 4 | # For Node.js providers |
| 5 | npm install --save-dev @pact-foundation/pact-node |
consumer.test.ts
TypeScript
| 1 | // Consumer test: define what the frontend expects from the API |
| 2 | import { PactV4, MatchersV3 } from "@pact-foundation/pact"; |
| 3 | import path from "path"; |
| 4 | |
| 5 | const provider = new PactV4({ |
| 6 | consumer: "WebFrontend", |
| 7 | provider: "UserAPI", |
| 8 | dir: path.resolve(process.cwd(), "pacts"), |
| 9 | logLevel: "warn", |
| 10 | }); |
| 11 | |
| 12 | describe("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) |
| 38 | async 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 |
| 2 | import { Verifier } from "@pact-foundation/pact"; |
| 3 | |
| 4 | describe("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 |
| 2 | version: '3.8' |
| 3 | services: |
| 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 | |
| 25 | volumes: |
| 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