
Contract Testing: Bridging the Gap between Playwright and Pact
In modern distributed systems, E2E tests often become a bottleneck. They are notoriously slow, flaky, and heavily dependent on the backend being "up and healthy" in a shared staging environment.
Contract Testing solves this by shifting the focus from "Does the whole system work?" to "Do these two parts still speak the same language?".
The Core Concept: Consumer vs. Provider
- The Consumer (React App): Defines a "Contract" (Pact file) specifying exactly what data it needs from the API.
- The Provider (The API Service): Must verify that it can fulfill that contract without breaking the structure.
- The Broker (PactFlow): A service that hosts the contracts and acts as the "source of truth" for both teams.
Step 1: Generating the Contract (The Consumer)
Using @pact-foundation/pact, we write a test that acts as a mock server. If the React component works with this mock, a .json contract is generated.
import { PactV3 } from '@pact-foundation/pact';
const provider = new PactV3({
consumer: 'Frontend-App',
provider: 'User-Service-API',
});
// Defining the expected interaction
provider.addInteraction({
states: [{ description: 'user with ID 1 exists' }],
uponReceiving: 'a request for user details',
withRequest: {
method: 'GET',
path: '/users/1',
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: 1,
username: 'johndoe',
role: 'admin'
},
},
});
Step 2: Validating UI with Playwright
Instead of hitting a real (and unpredictable) staging API, Playwright runs against the Pact Mock Server. This ensures that your UI tests are always based on a validated contract.
// playwright.config.ts
import { test, expect } from '@playwright/test';
test('should render user profile based on contract', async ({ page }) => {
// Page points to the Pact Mock Server port
await page.goto('http://localhost:1234/profile/1');
await expect(page.locator('.username')).toHaveText('johndoe');
});
Step 3: Provider Verification (The Contract Provider)
The API team pulls the contract from the Broker and runs it against their real endpoints. If they ever change username to user_name, the verification fails immediately.
// Provider verification script
const { Verifier } = require('@pact-foundation/pact');
new Verifier().verifyProvider({
provider: 'User-Service-API',
providerBaseUrl: 'http://localhost:8080',
pactBrokerUrl: '[https://your-tenant.pactflow.io](https://your-tenant.pactflow.io)',
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
publishVerificationResult: true,
});
Why this matters for your project
- Speed: Contract tests run in milliseconds compared to minutes for full E2E.
- Confidence: You can deploy the Frontend knowing the Backend won't break it.
- Documentation: The Pact file itself is living documentation of your API.
Essential Tools:
- Pact JS GitHub
- PactFlow: The most popular Contract Broker service.
- Playwright: Official Docs.