Pact is a consumer-driven contract testing framework that ensures your API clients and servers can communicate effectively without needing to run full integration tests.
Let’s see Pact in action. Imagine we have a customer-service (the API provider) and a billing-service (the API consumer). The billing-service needs to fetch customer details from customer-service.
First, the consumer (billing-service) defines its expectations. This is done in a billing_service_test.py file:
# billing_service_test.py
from pact import Consumer, Provider
import pytest
import requests
# Define the consumer and provider
pact = Consumer('BillingService').has_pact_with(Provider('CustomerService'))
# Start the mock service
pact.start_service()
# Define the interaction: what the consumer expects
def test_get_customer_details():
expected_customer = {
"id": 123,
"name": "Alice Smith",
"email": "alice.smith@example.com"
}
pact.upon_receiving("a request for customer 123") \
.with_request('get', '/customers/123') \
.will_respond_with(200, body=expected_customer)
# Make the actual request to the mock service
response = requests.get(f'{pact.mock_service.url}/customers/123')
assert response.status_code == 200
assert response.json() == expected_customer
# Stop the mock service and write the pact file after all tests
@pytest.fixture(scope='module')
def pact_fixture():
yield
pact.stop_service()
When pytest runs billing_service_test.py, Pact starts a mock server on a specific port (e.g., 127.0.0.1:1234). The test makes a request to this mock server. If the request matches the defined interaction (GET /customers/123), the mock server returns the specified expected_customer body. The test asserts that the response is as expected. Crucially, after the test completes, Pact writes a billing-service-customer-service.json file. This file is the "pact" – a contract detailing the consumer’s expectations.
Now, the provider (customer-service) uses this pact file to verify its own API. In customer_service_test.py:
# customer_service_test.py
from pact import Consumer, Provider
import pytest
import requests
# Define the consumer and provider
pact = Consumer('BillingService').has_pact_with(Provider('CustomerService'))
# Load the pact file generated by the consumer
pact_json = pact.from_path('pacts/billing-service-customer-service.json')
# Define the verification
def test_customer_service_contract():
# This request is to the *actual* customer-service API, not a mock
response = pact_json.verify_provider_app_func(
lambda: requests.get('http://localhost:5000/customers/123') # Replace 5000 with your actual API port
)
assert response.status_code == 200
assert response.json()['id'] == 123
assert response.json()['name'] == 'Alice Smith'
When pytest runs customer_service_test.py, Pact reads the billing-service-customer-service.json file. It then sends requests (defined in the pact file) to the actual customer-service API. If the customer-service responds in a way that satisfies all the interactions defined in the pact, the verification passes. If the customer-service API changes and no longer meets the contract (e.g., it starts returning a different status code or response body for /customers/123), this verification test will fail, alerting the provider team that they’ve broken the contract.
The core problem Pact solves is the brittle nature of traditional integration tests. These tests often require spinning up multiple services, which is slow, resource-intensive, and prone to timing issues. Contract testing, on the other hand, isolates the interaction. The consumer defines what it needs, and the provider verifies it can meet those specific needs. This allows teams to develop and deploy services independently, as long as they honor their contracts.
The mental model for Pact is:
- Consumer Defines Expectations: The consumer writes tests that interact with a Pact mock server. These tests describe the requests the consumer will make and the responses it expects.
- Pact File is Generated: After the consumer tests run successfully, Pact writes a JSON file (the pact) that details these interactions. This file is the contract.
- Provider Verifies Contract: The provider takes the pact file from the consumer and uses it to verify its own API. It sends requests to its actual service and checks if the responses match what the consumer expects.
The "pact broker" is a crucial piece of infrastructure often used in CI/CD pipelines. It acts as a central repository for pact files. When a consumer publishes its pact, the broker can then notify the provider that a new contract is available. This enables automated verification of provider deployments against the latest consumer contracts, preventing deployment of incompatible versions.
A common point of confusion is understanding that the consumer tests against a mock service, while the provider tests against its actual service. The pact file acts as the bridge, ensuring the expectations set by the consumer are met by the provider’s real implementation.
The next step is integrating Pact into your CI/CD pipeline to automate pact publishing and verification.