Contract Testing: Bridging the Gap between Playwright and Pact
March 24, 2026QA Automation

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

  1. The Consumer (React App): Defines a "Contract" (Pact file) specifying exactly what data it needs from the API.
  2. The Provider (The API Service): Must verify that it can fulfill that contract without breaking the structure.
  3. 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:

Willy Osorto

Author: Willy Osorto

Connect: