Skip to main content

Lightweight consumer-driven contract testing for microservices APIs

Project description

:handshake: pactship

Broker-less contract testing for microservices

GitHub Stars License Python Tests


Define contracts. Verify providers. Catch breaking changes before they ship.

Quick Start | CLI | Matchers | API | Architecture


Why This Exists

Microservices break in production when providers change their APIs without telling consumers. The consumer expects GET /users/1 to return { id, name, email } -- the provider ships a rename from name to full_name and three downstream services crash at 2 AM.

Existing contract testing tools solve this -- but they bring heavyweight infrastructure with them. Pact JVM needs a broker server. Spring Cloud Contract requires a JVM toolchain. Both demand CI/CD plumbing that takes longer to set up than the contracts themselves.

pactship is a zero-infrastructure, file-based contract testing tool for Python. Write contracts in YAML or JSON, verify them against live providers with async HTTP, diff versions to detect breaking changes, and store everything locally -- no broker server needed, no background processes, no Docker containers.

  • No infrastructure -- contracts live as files in your repo, verified locally or in CI
  • Fluent Python DSL -- build contracts programmatically with type-checked builders
  • 16 matcher types -- from exact match to regex, UUID, email, ISO dates, nullable, and range
  • Breaking change detection -- diff two contract versions with breaking/non-breaking classification

Requirements

  • Python 3.10+
  • Dependencies: click, pyyaml, jsonschema, rich, httpx

Quick Start

pip install pactship

Define a Contract (YAML)

consumer: order-service
provider: user-api
interactions:
  - description: Get user by ID
    request:
      method: GET
      path: /users/1
    response:
      status: 200
      body:
        id: 1
        name: Alice
        email: alice@example.com

Define a Contract (Python DSL)

from pactship import ContractBuilder, InteractionBuilder
from pactship import like, email_match, integer_match

contract = (
    ContractBuilder("order-service", "user-api")
    .add_interaction(
        InteractionBuilder("Get user by ID")
        .given("user 1 exists")
        .with_request("GET", "/users/1")
        .will_respond_with(200)
        .with_response_body(
            {"id": 1, "name": "Alice", "email": "alice@example.com"},
            matchers={
                "body.id": integer_match(),
                "body.name": like("string"),
                "body.email": email_match(),
            },
        )
        .build()
    )
    .build()
)

Verify Against a Provider

pactship verify contract.yaml http://localhost:8080

Detect Breaking Changes

pactship diff old-contract.yaml new-contract.yaml

Publish to Local Broker

pactship publish contract.yaml --broker-dir .pactship
pactship list --broker-dir .pactship

How It Works

Define          Verify           Diff             Report
──────────      ──────────       ──────────       ──────────
YAML/JSON   →   Provider     →   Version A    →   Breaking
  or DSL        Verification      vs B             changes
                (async HTTP)                       classified
  1. Define -- write contracts as YAML/JSON files or build them with the fluent Python DSL
  2. Verify -- run contracts against a live provider using async HTTP via httpx
  3. Diff -- compare two contract versions to detect breaking vs non-breaking changes
  4. Report -- get results in JSON, JUnit XML, Markdown, or TAP format

Features

Contract Definition

  • Fluent DSL -- ContractBuilder and InteractionBuilder with method chaining
  • YAML and JSON -- read and write contracts in both formats via load_contract / save_contract
  • Provider states -- define preconditions with .given("user 1 exists")
  • Request/response specs -- method, path, headers, query params, body, status code

Matcher System (16 Types)

  • Type matching -- like("string") matches any string, integer_match() matches any int
  • Regex patterns -- regex(r"\d{3}-\d{4}") for custom format validation
  • Structural matchers -- array_like(min_len), each_like(example) for arrays
  • Domain matchers -- email_match(), uuid_match(), iso_date(), iso_datetime()
  • Constraint matchers -- range_match(0, 100), any_of(["a", "b"]), nullable("string")

Verification

  • Async HTTP verification -- verify contracts against running providers using httpx
  • Mock provider -- in-process mock for consumer-side testing without a real server
  • Request matching -- method, path, headers, query params validated against spec
  • Matcher evaluation -- response body verified against all declared matchers
  • Provider state setup -- optional setup URL for test data preparation
  • Timeout configuration -- per-verification timeout control

Breaking Change Detection

  • Contract diffing -- diff_contracts(old, new) returns a structured diff report
  • Change classification -- each change tagged as breaking or non-breaking
  • Interaction-level diff -- detects added, removed, and modified interactions
  • Field-level diff -- tracks changes to individual request/response fields

Local Broker

  • Filesystem-based storage -- contracts stored as files in a configurable directory
  • Versioning -- publish contracts with version numbers, retrieve specific versions
  • Verification history -- track which provider versions were verified against which contracts
  • No server needed -- everything runs locally, works offline, no network dependency

Contract Linting

  • REST best practices -- validate path naming, HTTP method usage, status codes
  • Custom lint rules -- extensible linting with severity-based issue reporting
  • Pre-publish validation -- catch contract quality issues before sharing

OpenAPI Import

  • OpenAPI 3.x conversion -- convert OpenAPI specs to pactship contracts automatically
  • Path and method extraction -- generates interactions from OpenAPI path definitions
  • Response schema mapping -- maps OpenAPI response schemas to pactship response specs

Code Generation

  • CRUD generator -- generate_crud_contract() creates full CRUD contracts from resource specs
  • Endpoint generator -- generate_from_endpoints() builds contracts from endpoint definitions
  • Customizable templates -- configure generated interactions per HTTP method

Service Graph

  • Dependency visualization -- build service dependency graphs from contract sets
  • Mermaid diagram output -- generate Mermaid diagrams for documentation
  • Cycle detection -- identify circular dependencies between services

Compatibility Matrix

  • Version tracking -- CompatibilityMatrix tracks which consumer/provider versions work together
  • Matrix queries -- check compatibility between specific version pairs
  • History management -- add, query, and export compatibility records

Reporting

  • JSON reports -- structured verification results as JSON
  • JUnit XML -- integrate with CI systems expecting JUnit format
  • Markdown reports -- human-readable reports for PR comments
  • TAP output -- Test Anything Protocol for pipeline integration

Configuration

  • File-based config -- .pactship.yaml or .pactship.json project configuration
  • Environment variables -- PACTSHIP_BROKER_DIR, PACTSHIP_TIMEOUT, etc.
  • Priority ordering -- env vars override file config, file config overrides defaults

Statistics

  • Method distribution -- analyze HTTP method usage across contracts
  • Path coverage -- track which API paths are covered by contracts
  • Complexity metrics -- measure contract complexity and matcher density

Lifecycle Hooks

  • Before/after verification -- run custom logic around verification cycles
  • Setup/teardown -- provider state preparation and cleanup
  • Hook registration -- register hooks via the HookRegistry

Contract Transformation

  • Path rewriting -- transform contract paths for different environments
  • Header injection -- add/modify headers across all interactions
  • Body transformation -- apply transforms to request/response bodies

Filtering

  • Interaction filters -- filter by HTTP method, path pattern, or description
  • Tag-based filtering -- filter contracts by metadata tags
  • Composable filters -- combine multiple filters with AND/OR logic

CLI Commands

# Validate a contract file
pactship validate contract.yaml

# Verify against a live provider
pactship verify contract.yaml http://localhost:8080 \
  --timeout 30 \
  --header "Authorization:Bearer token" \
  --setup-url http://localhost:8080/_setup \
  --output report.json

# Diff two contract versions
pactship diff v1/contract.yaml v2/contract.yaml

# Publish to local broker
pactship publish contract.yaml \
  --broker-dir .pactship \
  --version 1.0.0 \
  --tag production

# List contracts in broker
pactship list --broker-dir .pactship

# Convert between formats
pactship convert contract.yaml contract.json
Command Description
pactship validate <file> Validate contract file syntax and structure
pactship verify <file> <url> Verify contract against a running provider
pactship diff <old> <new> Compare two contract versions for breaking changes
pactship publish <file> Publish contract to local filesystem broker
pactship list List all contracts stored in the broker
pactship convert <in> <out> Convert between YAML and JSON formats

Matchers

Matcher Description Example
exact(value) Exact value match exact("hello")
like(type) Type-based match like("string")
regex(pattern) Regex pattern match regex(r"\d{3}-\d{4}")
range_match(min, max) Numeric range constraint range_match(0, 100)
array_like(min_len) Array with minimum length array_like(1)
each_like(example) Each element matches structure each_like({"id": 0})
any_of(values) One of allowed values any_of(["a", "b"])
nullable(type) Null or specified type nullable("string")
iso_date() ISO 8601 date string iso_date()
iso_datetime() ISO 8601 datetime string iso_datetime()
uuid_match() UUID v4 format uuid_match()
email_match() Email address format email_match()
integer_match() Integer value integer_match()
decimal_match() Decimal number decimal_match()
boolean_match() Boolean value boolean_match()
string_match() String value string_match()

Programmatic API

Contract Building

from pactship import ContractBuilder, InteractionBuilder
from pactship import like, regex, integer_match, email_match

contract = (
    ContractBuilder("order-service", "user-api")
    .with_metadata({"version": "1.0.0"})
    .add_interaction(
        InteractionBuilder("Get user by ID")
        .given("user 1 exists")
        .with_request("GET", "/users/1")
        .with_request_header("Accept", "application/json")
        .will_respond_with(200)
        .with_response_header("Content-Type", "application/json")
        .with_response_body(
            {"id": 1, "name": "Alice", "email": "alice@example.com"},
            matchers={
                "body.id": integer_match(),
                "body.name": like("string"),
                "body.email": email_match(),
            },
        )
        .build()
    )
    .build()
)

Contract I/O

from pactship import save_contract, load_contract

# Save to YAML or JSON (auto-detected from extension)
save_contract(contract, "contracts/user-api.yaml")

# Load from file
loaded = load_contract("contracts/user-api.yaml")

Verification

from pactship import ProviderVerifier, MockProvider

# Verify against a live provider
verifier = ProviderVerifier(base_url="http://localhost:8080", timeout=30.0)
report = await verifier.verify(contract)
print(f"Passed: {report.success}")
for result in report.results:
    print(f"  {result.interaction}: {'PASS' if result.passed else 'FAIL'}")

# Use mock provider for consumer testing
mock = MockProvider(contract)
response = mock.handle_request("GET", "/users/1")
assert response.status == 200

Breaking Change Detection

from pactship import diff_contracts

diff = diff_contracts(old_contract, new_contract)
print(f"Breaking changes: {diff.has_breaking_changes}")
for change in diff.changes:
    print(f"  [{change.change_type}] {change.description}")

Local Broker

from pactship import ContractBroker

broker = ContractBroker(broker_dir=".pactship")
broker.publish(contract, version="1.0.0", tags=["production"])
contracts = broker.list_contracts()
specific = broker.get_contract("order-service", "user-api", version="1.0.0")

OpenAPI Import

from pactship.openapi import openapi_to_contracts

contracts = openapi_to_contracts("openapi.yaml", consumer="my-service")
for contract in contracts:
    save_contract(contract, f"contracts/{contract.provider}.yaml")

Service Graph

from pactship import ServiceGraph

graph = ServiceGraph()
graph.add_contract(contract)
mermaid = graph.to_mermaid()
print(mermaid)
# graph TD
#   order-service --> user-api

Contract Linting

from pactship import lint_contract

result = lint_contract(contract)
print(f"Passed: {result.passed}")
for issue in result.issues:
    print(f"  [{issue.severity}] {issue.rule}: {issue.message}")

Reporting

from pactship.reporting import (
    report_json,
    report_junit,
    report_markdown,
    report_tap,
)

# Generate reports in multiple formats
json_report = report_json(verification_report)
junit_xml = report_junit(verification_report)
markdown = report_markdown(verification_report)
tap_output = report_tap(verification_report)

Statistics

from pactship.stats import contract_stats

stats = contract_stats(contract)
print(f"Methods: {stats['method_distribution']}")
print(f"Paths: {stats['path_count']}")
print(f"Matchers: {stats['matcher_count']}")

Architecture

pactship/
  __init__.py         # Public API exports (54 symbols)
  models.py           # Core data models (Contract, Interaction, Matcher, etc.)
  dsl.py              # Fluent builder DSL (ContractBuilder, InteractionBuilder)
  matchers.py         # 16 matcher types (exact, like, regex, range, etc.)
  validator.py        # Contract structure validation
  verifier.py         # Async HTTP provider verification + MockProvider
  contract_io.py      # YAML/JSON serialization and deserialization
  schema.py           # JSON Schema generation from contracts
  diff.py             # Contract version diffing with change classification
  broker.py           # Filesystem-based contract broker with versioning
  cli.py              # Click CLI (validate, verify, diff, publish, list, convert)
  config.py           # File + env var configuration loading
  generator.py        # CRUD and endpoint-based contract generation
  graph.py            # Service dependency graph with Mermaid output
  matrix.py           # Consumer/provider compatibility matrix
  openapi.py          # OpenAPI 3.x to pactship contract conversion
  linter.py           # Contract linting with REST best practice rules
  reporting.py        # Multi-format reports (JSON, JUnit, Markdown, TAP)
  stats.py            # Contract statistics and complexity metrics
  hooks.py            # Lifecycle hook registry (before/after verification)
  transform.py        # Contract transformation (paths, headers, bodies)
  filters.py          # Interaction filtering (method, path, tags)

Data Flow

                    ┌─────────────┐
                    │  YAML/JSON  │
                    │   Contract  │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
        ┌─────▼─────┐ ┌───▼───┐ ┌─────▼─────┐
        │ Validator  │ │ Diff  │ │  Linter   │
        └─────┬─────┘ └───┬───┘ └─────┬─────┘
              │            │            │
        ┌─────▼─────┐ ┌───▼───┐ ┌─────▼─────┐
        │ Verifier  │ │Report │ │  Issues   │
        │(async HTTP)│ │       │ │           │
        └─────┬─────┘ └───────┘ └───────────┘
              │
        ┌─────▼─────┐
        │  Report   │
        │JSON/JUnit │
        │ MD / TAP  │
        └───────────┘

CI/CD Integration

GitHub Actions

name: Contract Tests
on: [push, pull_request]

jobs:
  contracts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install pactship
      - run: |
          for f in contracts/*.yaml; do
            pactship validate "$f"
          done
      - run: pactship diff contracts/v1.yaml contracts/v2.yaml || true

Pre-commit Hook

#!/bin/sh
for f in contracts/*.yaml; do
  pactship validate "$f" || exit 1
done

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pactship-0.1.0.tar.gz (72.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pactship-0.1.0-py3-none-any.whl (47.0 kB view details)

Uploaded Python 3

File details

Details for the file pactship-0.1.0.tar.gz.

File metadata

  • Download URL: pactship-0.1.0.tar.gz
  • Upload date:
  • Size: 72.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for pactship-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f1352479d9e4e158012d704009ee072f8cf912ef8c367d358a805a7720a4e54d
MD5 85697f7e91dfb6df21da01184f682d46
BLAKE2b-256 6298b20e76cfe3a430da0e4de40e42799ac95404cb1f871aa41f7ffdf4f7b400

See more details on using hashes here.

File details

Details for the file pactship-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pactship-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 47.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for pactship-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5946b00d854060db18842a6b0c08643158572d92310c1c92eac5b765582c1765
MD5 81308bbd496302377f28a4f9d4dcef9b
BLAKE2b-256 6c1c10cbc295935ccbdf8455fca518527974e7ff14a221ec01d6ba12eeec5bd8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page