Skip to content

JSLEEKR/pactship

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🤝 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

About

Lightweight consumer-driven contract testing for microservices APIs

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages