AI Tools Compared

AI Tools for Automated Contract Testing 2026

Contract testing catches integration failures before microservices are deployed together. Unlike end-to-end tests, contract tests are fast, deterministic, and don’t require a running environment. AI tools can generate the boilerplate — but you need to verify the contracts reflect your actual API.

Pact Consumer Tests (Python)

Prompt to Claude:

Write a Pact consumer test in Python for an OrderService that calls UserService.
The consumer calls GET /users/{id} and expects a 200 response with
{id: int, name: string, email: string}. Use pact-python. The user id is 42.
Include setup, pact definition, and actual request verification.

Claude output:

# tests/contract/test_user_service_consumer.py
import pytest
import requests
from pact import Consumer, Provider

PACT_DIR = "pacts"
PACT_HOST = "localhost"
PACT_PORT = 1234

@pytest.fixture(scope="module")
def pact():
    pact = Consumer("OrderService").has_pact_with(
        Provider("UserService"),
        host_name=PACT_HOST,
        port=PACT_PORT,
        pact_dir=PACT_DIR,
        log_dir="logs/pact",
    )
    pact.start_service()
    yield pact
    pact.stop_service()


def test_get_user_by_id(pact):
    expected = {
        "id": 42,
        "name": "Alice Smith",
        "email": "alice@example.com",
    }

    (
        pact
        .given("user with id 42 exists")
        .upon_receiving("a request for user 42")
        .with_request(
            method="GET",
            path="/users/42",
            headers={"Accept": "application/json"},
        )
        .will_respond_with(
            status=200,
            headers={"Content-Type": "application/json"},
            body={
                "id": 42,
                "name": "Alice Smith",
                "email": "alice@example.com",
            },
        )
    )

    with pact:
        result = requests.get(
            f"http://{PACT_HOST}:{PACT_PORT}/users/42",
            headers={"Accept": "application/json"},
        )

    assert result.status_code == 200
    body = result.json()
    assert body["id"] == 42
    assert "name" in body
    assert "email" in body

Claude correctly sets up the Pact mock server, defines the interaction with a state (“user with id 42 exists”), and verifies via an actual HTTP request to the mock. The pact file is written to PACT_DIR for sharing with the provider.

Pact Provider Verification (TypeScript)

Prompt:

Write the provider-side Pact verification for UserService in TypeScript/Jest.
Load pact files from the pacts directory. The real UserService runs on port 3001.
Add a state handler for "user with id 42 exists" that seeds test data.

Claude output:

// tests/contract/user-service.provider.spec.ts
import { Verifier } from "@pact-foundation/pact";
import path from "path";
import { app } from "../../src/app";
import { userRepository } from "../../src/repositories/userRepository";
import * as http from "http";

describe("UserService provider contract verification", () => {
  let server: http.Server;

  beforeAll((done) => {
    server = app.listen(3001, done);
  });

  afterAll((done) => {
    server.close(done);
  });

  it("validates consumer contracts", () => {
    return new Verifier({
      provider: "UserService",
      providerBaseUrl: "http://localhost:3001",
      pactUrls: [
        path.resolve(__dirname, "../../pacts/OrderService-UserService.json"),
      ],
      stateHandlers: {
        "user with id 42 exists": async () => {
          await userRepository.upsert({
            id: 42,
            name: "Alice Smith",
            email: "alice@example.com",
          });
        },
        "no users exist": async () => {
          await userRepository.clear();
        },
      },
      publishVerificationResult: process.env.CI === "true",
      providerVersion: process.env.GIT_COMMIT || "local",
    }).verifyProvider();
  });
});

Using Pact Matchers for Flexible Contracts

Pact tests become fragile when you hardcode exact field values. Claude correctly suggests matchers when asked:

Prompt:

Update the consumer test to use Pact matchers so the contract validates field types
rather than exact values. The id should be an integer, name and email should be strings.
from pact import Consumer, Provider, Like, Term

def test_get_user_by_id_with_matchers(pact):
    (
        pact
        .given("user with id 42 exists")
        .upon_receiving("a request for user 42")
        .with_request(
            method="GET",
            path="/users/42",
            headers={"Accept": "application/json"},
        )
        .will_respond_with(
            status=200,
            headers={"Content-Type": "application/json"},
            body={
                "id": Like(42),          # any integer
                "name": Like("Alice"),   # any string
                "email": Term(
                    r"[^@]+@[^@]+\.[^@]+",
                    "alice@example.com"
                ),                       # validated by regex
            },
        )
    )

This is the right approach for production contracts: the provider can return any valid user, not just Alice with id 42. Copilot and ChatGPT rarely suggest matchers unprompted — they default to exact values, creating brittle contracts that break on test data changes.

OpenAPI Contract Validation

For teams using OpenAPI specs as contracts, Claude generates validators using schemathesis or dredd:

Prompt:

Write a Python test that validates a running FastAPI service against its OpenAPI spec.
Use schemathesis to test all endpoints with generated inputs. Include stateful testing
for the /users and /orders endpoints where POST /orders references a user.
# tests/contract/test_openapi_contract.py
import schemathesis
from schemathesis.stateful import Stateful

# Load schema from running service
schema = schemathesis.from_uri("http://localhost:8000/openapi.json")

@schema.parametrize()
def test_api_contract(case):
    """Test all endpoints conform to the OpenAPI spec."""
    response = case.call()
    case.validate_response(response)


# Stateful test: POST /users → POST /orders (using returned user ID)
@schema.parametrize(stateful=Stateful.links)
def test_stateful_contract(case):
    """Test stateful sequence: create user then create order for that user."""
    response = case.call()
    case.validate_response(response)

Add schemathesis run http://localhost:8000/openapi.json --checks all to your CI pipeline for automated contract checking without writing tests manually.

Dredd Alternative for API Blueprint / OpenAPI

# dredd.yml
dry-run: null
hookfiles: tests/contract/hooks.js
language: nodejs
sandbox: false
server: uvicorn app.main:app --port 8000
server-wait: 3
reporter: [dot]
custom:
  apiaryApiKey: null
blueprint: openapi.yaml
endpoint: http://localhost:8000
// tests/contract/hooks.js
const hooks = require('hooks');

hooks.before('/users/{id} > GET', (transaction, done) => {
  // Seed test data before running this contract test
  transaction.request.params.id = '42';
  done();
});

hooks.after('/users/{id} > GET', (transaction, done) => {
  // Assert extra conditions not in the spec
  const body = JSON.parse(transaction.real.body);
  if (!body.email.includes('@')) {
    transaction.fail = 'Email format invalid';
  }
  done();
});

gRPC Contract Testing

Prompt:

Write a Go test that validates a gRPC UserService implementation against its protobuf contract.
Check that all required fields are returned and that status codes are correct for missing users.
Use grpc/codes for status checking.
// contract/user_service_contract_test.go
package contract_test

import (
    "context"
    "testing"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/status"
    pb "github.com/example/userservice/gen"
)

func TestUserServiceContract(t *testing.T) {
    conn, err := grpc.NewClient(
        "localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        t.Fatalf("failed to connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewUserServiceClient(conn)
    ctx := context.Background()

    t.Run("GetUser returns required fields", func(t *testing.T) {
        resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 1})
        if err != nil {
            t.Fatalf("GetUser failed: %v", err)
        }
        if resp.Id == 0 {
            t.Error("expected non-zero id")
        }
        if resp.Name == "" {
            t.Error("expected non-empty name")
        }
        if resp.Email == "" {
            t.Error("expected non-empty email")
        }
    })

    t.Run("GetUser returns NOT_FOUND for missing user", func(t *testing.T) {
        _, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 999999})
        if err == nil {
            t.Fatal("expected error for missing user")
        }
        st, ok := status.FromError(err)
        if !ok {
            t.Fatalf("expected gRPC status error, got %T", err)
        }
        if st.Code() != codes.NotFound {
            t.Errorf("expected NOT_FOUND, got %s", st.Code())
        }
    })
}

Extending the gRPC Tests for Pagination

Claude also handles streaming RPC contract tests correctly:

t.Run("ListUsers streams all results with pagination", func(t *testing.T) {
    stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{PageSize: 10})
    if err != nil {
        t.Fatalf("ListUsers failed: %v", err)
    }

    var users []*pb.User
    for {
        user, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            t.Fatalf("stream error: %v", err)
        }
        if user.Id == 0 || user.Email == "" {
            t.Errorf("incomplete user record: %+v", user)
        }
        users = append(users, user)
    }

    if len(users) == 0 {
        t.Error("expected at least one user from stream")
    }
})

Integrating Contract Tests into CI

A practical GitHub Actions workflow that runs consumer tests, publishes pacts, and triggers provider verification:

# .github/workflows/contract-tests.yml
name: Contract Tests

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  consumer-contract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install pact-python pytest requests
      - run: pytest tests/contract/test_user_service_consumer.py -v
      - name: Publish pact to Pact Broker
        if: github.ref == 'refs/heads/main'
        env:
          PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_URL }}
          PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
        run: |
          pact-broker publish pacts/ \
            --consumer-app-version ${{ github.sha }} \
            --branch ${{ github.ref_name }}

  provider-contract:
    needs: consumer-contract
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run test:contract:provider
        env:
          DATABASE_URL: postgresql://postgres:test@localhost/testdb
          PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
          GIT_COMMIT: ${{ github.sha }}

AI Tool Comparison for Contract Tests

Task Claude Copilot ChatGPT
Pact consumer setup Excellent Good Good
Pact matchers (type-based) Suggests unprompted Rarely suggests Rarely suggests
State handlers Strong Partial Partial
OpenAPI schemathesis Strong Weak Moderate
gRPC contract tests Strong Weak Moderate
CI integration Complete workflow Partial Partial

Claude is strongest for gRPC and OpenAPI contract testing, which require more domain knowledge than REST/JSON Pact tests that all tools handle well. The matcher suggestion behavior is the biggest practical difference — exact-value contracts create maintenance burden that type-based matchers avoid entirely.

Built by theluckystrike — More at zovo.one