contract-testing

Consumer-driven contract testing with Pact, schema validation, and API compatibility verification.

39 stars

Best use case

contract-testing is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Consumer-driven contract testing with Pact, schema validation, and API compatibility verification.

Teams using contract-testing should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/contract-testing/SKILL.md --create-dirs "https://raw.githubusercontent.com/InugamiDev/ultrathink-oss/main/.claude/skills/contract-testing/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/contract-testing/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How contract-testing Compares

Feature / Agentcontract-testingStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Consumer-driven contract testing with Pact, schema validation, and API compatibility verification.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Consumer-Driven Contract Testing

## Purpose

Provide expert guidance on consumer-driven contract testing (CDCT) with Pact, schema validation with Zod/JSON Schema, API compatibility verification, and CI integration. Ensures APIs evolve without breaking consumers in microservice architectures.

## Why Contract Testing

```
┌────────────────────────────────────────────────┐
│           Testing Pyramid for APIs             │
│                                                │
│              ┌──────────┐                      │
│              │  E2E     │  Slow, brittle       │
│            ┌─┴──────────┴─┐                    │
│            │  Integration  │  Needs live deps   │
│          ┌─┴──────────────┴─┐                  │
│          │  Contract Tests   │  Fast, isolated  │  ← This skill
│        ┌─┴──────────────────┴─┐                │
│        │     Unit Tests        │                │
│        └───────────────────────┘                │
└────────────────────────────────────────────────┘
```

Contract tests verify that a **provider** API meets the expectations of its **consumers** without requiring both services to be running simultaneously.

## Pact Consumer Test (TypeScript)

The consumer defines what it expects from the provider:

```typescript
// consumer/tests/user-service.pact.test.ts
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
import { UserApiClient } from '../src/user-api-client';

const { like, eachLike, string, integer, datetime } = MatchersV3;

const provider = new PactV4({
  consumer: 'OrderService',
  provider: 'UserService',
  dir: './pacts', // output directory for contract files
});

describe('User Service API', () => {
  describe('GET /users/:id', () => {
    it('returns user details', async () => {
      await provider
        .addInteraction()
        .given('user 123 exists')
        .uponReceiving('a request for user 123')
        .withRequest('GET', '/users/123', (builder) => {
          builder.headers({ Accept: 'application/json' });
        })
        .willRespondWith(200, (builder) => {
          builder
            .headers({ 'Content-Type': 'application/json' })
            .jsonBody({
              id: integer(123),
              email: string('user@example.com'),
              name: string('Jane Doe'),
              createdAt: datetime("yyyy-MM-dd'T'HH:mm:ss.SSSX", '2024-01-15T10:30:00.000Z'),
              orders: eachLike({
                id: string('ord-001'),
                total: like(49.99),
              }),
            });
        })
        .executeTest(async (mockServer) => {
          const client = new UserApiClient(mockServer.url);
          const user = await client.getUser('123');

          expect(user.id).toBe(123);
          expect(user.email).toBeDefined();
          expect(user.orders).toBeInstanceOf(Array);
        });
    });

    it('returns 404 for unknown user', async () => {
      await provider
        .addInteraction()
        .given('user 999 does not exist')
        .uponReceiving('a request for non-existent user 999')
        .withRequest('GET', '/users/999')
        .willRespondWith(404, (builder) => {
          builder.jsonBody({
            error: string('Not Found'),
            message: string('User 999 not found'),
          });
        })
        .executeTest(async (mockServer) => {
          const client = new UserApiClient(mockServer.url);
          await expect(client.getUser('999')).rejects.toThrow('User 999 not found');
        });
    });
  });

  describe('POST /users', () => {
    it('creates a new user', async () => {
      await provider
        .addInteraction()
        .uponReceiving('a request to create a user')
        .withRequest('POST', '/users', (builder) => {
          builder
            .headers({ 'Content-Type': 'application/json' })
            .jsonBody({
              email: 'new@example.com',
              name: 'New User',
            });
        })
        .willRespondWith(201, (builder) => {
          builder.jsonBody({
            id: integer(),
            email: string('new@example.com'),
            name: string('New User'),
          });
        })
        .executeTest(async (mockServer) => {
          const client = new UserApiClient(mockServer.url);
          const user = await client.createUser({
            email: 'new@example.com',
            name: 'New User',
          });
          expect(user.id).toBeDefined();
        });
    });
  });
});
```

## Pact Provider Verification

The provider verifies it meets consumer expectations:

```typescript
// provider/tests/pact-verification.test.ts
import { Verifier } from '@pact-foundation/pact';
import { app } from '../src/app';

describe('Pact Provider Verification', () => {
  let server: any;

  beforeAll(async () => {
    // Start the real provider service
    server = app.listen(0);
  });

  afterAll(() => server.close());

  it('validates the expectations of OrderService', async () => {
    const port = server.address().port;

    await new Verifier({
      providerBaseUrl: `http://localhost:${port}`,
      provider: 'UserService',

      // Load pact files (local or from broker)
      pactUrls: ['../consumer/pacts/OrderService-UserService.json'],
      // OR from Pact Broker:
      // pactBrokerUrl: process.env.PACT_BROKER_URL,
      // pactBrokerToken: process.env.PACT_BROKER_TOKEN,
      // publishVerificationResult: true,
      // providerVersion: process.env.GIT_SHA,

      // Setup provider states
      stateHandlers: {
        'user 123 exists': async () => {
          await seedDatabase({
            id: 123,
            email: 'user@example.com',
            name: 'Jane Doe',
          });
        },
        'user 999 does not exist': async () => {
          await clearDatabase();
        },
      },
    }).verifyProvider();
  });
});
```

## Schema Validation with Zod

For simpler contract validation without Pact:

```typescript
// shared/contracts/user-contract.ts
import { z } from 'zod';

// Shared schema — defines the contract
export const UserResponseSchema = z.object({
  id: z.number().int().positive(),
  email: z.string().email(),
  name: z.string().min(1),
  createdAt: z.string().datetime(),
  orders: z.array(
    z.object({
      id: z.string(),
      total: z.number().nonnegative(),
    }),
  ).optional(),
});

export type UserResponse = z.infer<typeof UserResponseSchema>;

// Consumer test — validate against schema
describe('User API Client', () => {
  it('response matches contract schema', async () => {
    const response = await fetch('/users/123');
    const data = await response.json();

    // This throws if the response doesn't match
    const user = UserResponseSchema.parse(data);
    expect(user.id).toBe(123);
  });
});

// Provider test — validate output against schema
describe('GET /users/:id', () => {
  it('response conforms to contract', async () => {
    const response = await request(app).get('/users/123');

    expect(() => UserResponseSchema.parse(response.body)).not.toThrow();
  });
});
```

## OpenAPI-Based Contract Testing

```typescript
// tests/openapi-contract.test.ts
import { createDocument } from 'openapi-backend';
import SwaggerParser from '@apidevtools/swagger-parser';

describe('API Contract Compliance', () => {
  let api: any;

  beforeAll(async () => {
    api = await SwaggerParser.validate('./openapi.yaml');
  });

  it('GET /users/:id response matches OpenAPI spec', async () => {
    const response = await request(app).get('/users/123');

    // Validate response against the OpenAPI schema
    const schema = api.paths['/users/{id}'].get.responses['200'].content['application/json'].schema;
    const ajv = new Ajv();
    const validate = ajv.compile(schema);
    const valid = validate(response.body);

    expect(valid).toBe(true);
    if (!valid) console.error(validate.errors);
  });
});
```

## Pact Broker & CI Integration

**Publish contracts from consumer CI:**

```yaml
# consumer/.github/workflows/contract.yml
name: Consumer Contract Tests
on: [push]

jobs:
  contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npm run test:pact
      - name: Publish Pact to Broker
        run: |
          npx pact-broker publish ./pacts \
            --consumer-app-version=${{ github.sha }} \
            --branch=${{ github.ref_name }} \
            --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
            --broker-token=${{ secrets.PACT_BROKER_TOKEN }}
```

**Verify contracts in provider CI:**

```yaml
# provider/.github/workflows/contract.yml
name: Provider Contract Verification
on:
  push:
  # Webhook from Pact Broker when new contracts are published
  repository_dispatch:
    types: [pact-changed]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npm run test:pact:verify
        env:
          PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
          PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
          GIT_SHA: ${{ github.sha }}
```

**Can-I-Deploy check before deploying:**

```yaml
- name: Can I Deploy?
  run: |
    npx pact-broker can-i-deploy \
      --pacticipant=UserService \
      --version=${{ github.sha }} \
      --to-environment=production \
      --broker-base-url=${{ secrets.PACT_BROKER_URL }} \
      --broker-token=${{ secrets.PACT_BROKER_TOKEN }}
```

## Backward Compatibility Rules

**Safe changes (non-breaking):**
- Adding new optional fields to responses
- Adding new endpoints
- Adding new optional query parameters
- Widening accepted input types

**Breaking changes (require consumer updates):**
- Removing or renaming response fields
- Changing field types
- Adding required request fields
- Removing endpoints
- Changing URL paths

## Best Practices

1. **Consumer writes the contract** — The consumer defines what it needs, not what the provider offers.
2. **Use matchers, not exact values** — Pact matchers (`like`, `eachLike`, `integer`) validate shape, not data.
3. **Provider states setup test data** — Use `stateHandlers` to seed specific scenarios.
4. **Publish to a Pact Broker** — Central contract repository for all services.
5. **Run `can-i-deploy` before releases** — Verify compatibility before deploying to production.
6. **Version contracts by git SHA** — Track which commit generated each contract.
7. **Test error scenarios** — Contract should cover 404, 400, 401 responses.
8. **Keep contracts minimal** — Only test fields the consumer actually uses.
9. **Automate with webhooks** — Pact Broker triggers provider verification on new contracts.
10. **Use schema validation as a lightweight alternative** — Zod schemas for simpler setups without Pact.

## Common Pitfalls

| Pitfall | Problem | Fix |
|---------|---------|-----|
| Testing exact response values | Brittle tests that break on data changes | Use Pact matchers for shape, not exact values |
| Consumer tests too much | Contract covers fields consumer does not use | Only include fields the consumer actually consumes |
| No provider states | Provider verification fails due to missing test data | Implement `stateHandlers` for each `given()` clause |
| Skipping error contracts | Only happy path tested, 4xx/5xx breaks consumer | Add contract tests for error responses |
| Manual contract sharing | Pact JSON files emailed or committed | Use Pact Broker for automated contract exchange |
| Treating contracts as E2E tests | Slow, flaky, overloaded | Contracts verify schema and shape only; E2E tests verify behavior |

Related Skills

testing-toolkit

39
from InugamiDev/ultrathink-oss

Unified testing methodology toolkit — Testing Library (accessible queries, user-event, component testing), unit/integration/e2e/property-based testing patterns, test strategy design (pyramid/trophy/diamond, coverage goals), test fixtures (factories, builders, seeders, snapshots), API testing (Supertest, contract testing, endpoint validation). Keeps runtime-specific runners (vitest/playwright/cypress/promptfoo) separate.

load-testing

39
from InugamiDev/ultrathink-oss

Load testing with k6, Artillery, Locust — traffic simulation, performance baselines, and stress testing.

accessibility-testing

39
from InugamiDev/ultrathink-oss

Accessibility testing with axe-core, pa11y, Lighthouse, screen reader testing, and WCAG compliance verification

ultrathink

39
from InugamiDev/ultrathink-oss

UltraThink Workflow OS — 4-layer skill mesh with persistent memory and privacy hooks for complex engineering tasks. Routes prompts through intent detection to activate the right domain skills automatically.

ultrathink_review

39
from InugamiDev/ultrathink-oss

Multi-pass code review powered by UltraThink's quality gate — checks correctness, security (OWASP), performance, readability, and project conventions in a single structured pass.

ultrathink_memory

39
from InugamiDev/ultrathink-oss

Persistent memory system for UltraThink — search, save, and recall project context, decisions, and patterns across sessions using Postgres-backed fuzzy search with synonym expansion.

ui-design

39
from InugamiDev/ultrathink-oss

Comprehensive UI design system: 230+ font pairings, 48 themes, 65 design systems, 23 design languages, 30 UX laws, 14 color systems, Swiss grid, Gestalt principles, Pencil.dev workflow. Inherits ui-ux-pro-max (99 UX rules) + impeccable-frontend-design (anti-AI-slop). Triggers on any design, UI, layout, typography, color, theme, or styling task.

Zod

39
from InugamiDev/ultrathink-oss

> TypeScript-first schema validation with static type inference.

webinar-registration-page

39
from InugamiDev/ultrathink-oss

Build a webinar or live event registration page as a self-contained HTML file with countdown timer, speaker bio, agenda, and registration form. Triggers on: "build a webinar registration page", "create a webinar sign-up page", "event registration landing page", "live training registration page", "workshop sign-up page", "create a webinar page", "build an event page", "free webinar landing page", "live demo registration page", "online event page", "create a registration page for my webinar", "build a training event page".

webhooks

39
from InugamiDev/ultrathink-oss

Webhook design patterns — delivery, retry with exponential backoff, HMAC signature verification, payload validation, idempotency keys

web-workers

39
from InugamiDev/ultrathink-oss

Offload heavy computation from the main thread using Web Workers, SharedWorkers, and Comlink — structured messaging, transferable objects, and off-main-thread architecture patterns

web-vitals

39
from InugamiDev/ultrathink-oss

Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js