Skip to content

Contract Testing with Pact: Consumer-Driven API Contract Verification

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Contract Testing with Pact: Consumer. We cover key concepts, practical examples, and best practices to help you master this topic.

Contract testing with Pact validates that API providers meet consumer expectations through consumer-driven contracts (pacts), catching breaking changes before deployment and enabling safe microservice evolution.

What You'll Learn

How to implement consumer-driven contract testing with Pact, write consumer tests that generate pacts, verify provider compliance against pacts, publish pacts to a PactFlow broker, handle version matching and Webhooks, and integrate into CI/CD.

Why It Matters

Integration test suites that call real services are slow and brittle. Contract tests are fast (unit-level speed) and catch breaking API changes before they reach production. DodaTech uses Pact to verify contracts between 15+ Microservices.

Real-World Use

The Orders service consumes the Users API. The Orders team writes a Pact test defining expectations. The Users team runs the pact against their service. If they change the API in a breaking way, the Pact verification fails before deployment.

flowchart LR
    A["Consumer\n(Order Service)"] --> B["Write Pact\nTest"]
    B --> C["Generate\nPact File"]
    C --> D["Publish to\nPact Broker"]
    D --> E["Provider\n(User Service)"]
    E --> F["Verify Pact\nin CI"]
    F --> G{"Contract\nValid?"}
    G -->|Yes| H["Safe to\nDeploy"]
    G -->|No| I["Breaking\nChange Detected"]
    style A fill:#dbeafe,stroke:#2563eb
    style E fill:#bbf7d0,stroke:#16a34a
    style H fill:#bbf7d0,stroke:#16a34a
    style I fill:#fecaca,stroke:#dc2626

Consumer Test (Python)

# Consumer: Order Service tests its dependency on User Service
import atexit
from pact import Consumer, Provider

# Setup Pact
pact = Consumer("OrderService").has_pact_with(
    Provider("UserService"),
    pact_dir="./pacts",
    host_name="localhost",
    port=1234
)
pact.start_service()
atexit.register(pact.stop_service)

# Define the expected interaction
@pact.given("user with ID 42 exists")
@pact.upon_receiving("a request for user details")
@pact.with_request(method="GET", path="/users/42")
@pact.will_respond_with(
    status=200,
    headers={"Content-Type": "application/json"},
    body={
        "id": 42,
        "email": "user42@example.com",
        "name": "User Forty-Two",
        "role": "customer"
    }
)
def test_get_user():
    import requests
    result = requests.get("http://localhost:1234/users/42")
    assert result.status_code == 200
    assert result.json()["email"] == "user42@example.com"
    assert result.json()["name"] == "User Forty-Two"

# Run the test
test_get_user()

# Pact file is generated in ./pacts/OrderService-UserService.json
print("Pact generated successfully")

# Expected output:
# Pact generated successfully

Generated Pact File

// ./pacts/OrderService-UserService.json
{
  "consumer": {"name": "OrderService"},
  "provider": {"name": "UserService"},
  "interactions": [
    {
      "description": "a request for user details",
      "providerState": "user with ID 42 exists",
      "request": {
        "method": "GET",
        "path": "/users/42",
        "headers": {"Accept": "application/json"}
      },
      "response": {
        "status": 200,
        "headers": {"Content-Type": "application/json"},
        "body": {
          "id": 42,
          "email": "user42@example.com",
          "name": "User Forty-Two",
          "role": "customer"
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": {"version": "2.0.0"}
  }
}

Provider Verification (Python)

# Provider: User Service verifies it meets the Order Service contract
from pact import Verifier

verifier = Verifier(provider="UserService", provider_base_url="http://localhost:8000")

# Verify against a local pact file
output, info = verifier.verify_pacts(
    "./pacts/OrderService-UserService.json",
    verbose=True,
    provider_states_url="http://localhost:8000/_pact/provider_states"
)

print(f"Verification {'passed' if info == 0 else 'failed'}")
print(output)

# Expected output if verification passes:
# Verifying - a request for user details
#   Given user with ID 42 exists
#   GET /users/42
#   returns a response which
#     has status code 200
#     has a matching body
# Verification passed

# Provider needs to implement provider states
# Flask endpoint for provider states:
"""
@app.route("/_pact/provider_states", methods=["POST"])
def provider_states():
    data = request.json
    state = data.get("state")
    if state == "user with ID 42 exists":
        # Set up the database state
        db.session.add(User(id=42, email="user42@example.com", name="User Forty-Two"))
        db.session.commit()
    return jsonify({"result": "success"})
"""

Pact Broker Integration

# Publishing pacts to PactFlow Broker
import requests

def publish_pact_to_broker(pact_file, broker_url, version):
    """Upload pact file to broker."""
    with open(pact_file, "r") as f:
        pact_content = f.read()

    resp = requests.put(
        f"{broker_url}/pacts/provider/UserService/consumer/OrderService/version/{version}",
        data=pact_content,
        headers={"Content-Type": "application/json"}
    )
    print(f"Published pact: {resp.status_code}")
    return resp.status_code == 201

# CI integration
def ci_pact_workflow():
    """Run in CI after consumer tests pass."""
    # 1. Run consumer tests (generates pact file)
    # 2. Publish pact to broker with app version
    publish_pact_to_broker(
        "./pacts/OrderService-UserService.json",
        "https://dodatech.pactflow.io",
        os.environ.get("CI_COMMIT_SHA", "1.0.0")
    )

    # 3. Provider CI pipelines verify against latest pacts
    # 4. Can-I-deploy checks: pact-broker can-i-deploy

    print("Run: pact-broker can-i-deploy --pacticipant OrderService --version 1.0.0")
    print("Run: pact-broker can-i-deploy --pacticipant UserService --version 2.0.0")

# Expected output:
# Published pact: 201
# Run: pact-broker can-i-deploy --pacticipant OrderService --version 1.0.0
# Run: pact-broker can-i-deploy --pacticipant UserService --version 2.0.0

Common Mistakes

1. Testing Too Many Interactions

Each interaction in a pact should represent a real consumer use case. Testing every possible input/output pair creates brittle pacts. Test only the interactions your consumer actually uses.

2. Ignoring Provider States

Provider states set up the database state needed for verification. Without proper states, verification may fail because the expected data doesn't exist. Implement all states the consumer's pact expects.

3. Using Pact as a Replacement for Integration Tests

Pact tests verify the API contract, not end-to-end behavior. You still need integration tests for multi-step workflows. Pact + Integration tests form a complete testing Strategy.

4. Not Running Can-I-Deploy

Publishing pacts without checking can-i-deploy allows breaking changes through. Always run can-i-deploy before deployment to verify all consumers and providers are compatible.

5. Hardcoding Pact Broker URLs

Broker URLs differ per environment. Use environment variables for broker URL, API tokens, and version numbers. Never hardcode broker credentials in pact files.

Practice Questions

  1. What is consumer-driven contract testing?
  2. How is Pact different from integration tests?
  3. What is a provider state and why is it needed?
  4. What does can-i-deploy do?

Answers:

  1. Consumer-driven contract testing defines the API contract from the consumer's perspective. The consumer writes tests specifying expected responses. The provider verifies they meet those expectations. The contract is driven by actual consumer needs, not provider assumptions.
  2. Integration tests call real services end-to-end, testing the full stack. Pact tests are unit-speed tests that verify the API contract between services. Pact catches breaking changes; integration tests catch runtime issues.
  3. A provider state sets up the database/application state needed for a pact interaction. For "user with ID 42 exists", the provider must create that user before verification. States make pacts deterministic.
  4. can-i-deploy checks the Pact Broker to verify all consumers and providers are compatible with the given application version. It prevents deploying a version that would break existing consumers.

Challenge: Set up Pact contracts between two services (OrderService -> UserService): write consumer tests with 3 interactions, implement provider states, run provider verification, publish to a local Pact broker, implement can-i-deploy in CI, simulate a breaking change and verify it's caught, and fix the contract.

FAQ

What is the difference between Pact and OpenAPI?

OpenAPI describes what an API can do. Pact describes what a consumer actually uses. OpenAPI is provider-driven. Pact is consumer-driven. They complement each other: OpenAPI for documentation, Pact for contract verification.

How many pacts should I have?

One pact per consumer-provider pair. If 5 services consume the User API, there are 5 pacts. The User Service verifies all 5 pacts to ensure backward compatibility.

Can Pact test async messaging?

Yes, Pact supports message pacts for async/event-driven architectures (Kafka, SQS, RabbitMQ). Define the message format and content expected by the consumer, verified by the provider.

How do I handle optional fields in Pact?

Use Pact matchers: term (regex patterns), eachLike (array elements), somethingLike (type matching). These allow flexible matching instead of exact value comparisons.

What happens when a contract changes?

The consumer updates the pact, publishes the new version, and runs can-i-deploy. If the change is backward compatible, provider verification passes. If breaking, the provider must update before deployment.

Mini Project

Implement Pact contract testing between two services (Order Service and Inventory Service): write consumer tests with 3 interactions (check stock, reserve item, release item), implement provider states for each, run provider verification, publish to PactFlow broker, implement can-i-deploy in CI, introduce a breaking change and catch it, then fix the contract.

What's Next

Load Testing — performance test your APIs with k6.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro