Consumer Testing¶
Pact is a consumer-driven contract testing tool. The consumer specifies the expected interactions with the provider, which are used to create a contract. This contract is then used to verify that the provider meets the consumer's expectations.
sequenceDiagram
box Consumer Side
participant Consumer
participant P1 as Pact
end
box Provider Side
participant P2 as Pact
participant Provider
end
Consumer->>P1: GET /users/123
P1->>Consumer: 200 OK
Consumer->>P1: GET /users/999
P1->>Consumer: 404 Not Found
P1--)P2: Pact Broker
P2->>Provider: GET /users/123
Provider->>P2: 200 OK
P2->>Provider: GET /users/999
Provider->>P2: 404 Not Found
The consumer is the client that makes requests, and the provider is the server that responds. In most cases, the consumer is a front-end application and the provider is a back-end service; however, a back-end service may also require information from another service, making it a consumer of that service.
The core logic is implemented in Rust and exposed to Python through the core Pact FFI. This will help ensure feature parity between different language implementations and improve performance and reliability. This also brings compatibility with the latest Pact Specification (v4).
Note
For asynchronous interactions (e.g., message queues), the consumer refers to the service that processes the messages. This is not covered here, but further information is available in the Message Pact section of the Pact documentation.
Writing the Test¶
Note
The code below is an abridged version of this example.
Consumer Client¶
For example, consider a simple API client that interacts with a user provider service. The client has methods to get, create, and delete users. The user data model is defined using a dataclass.
from dataclasses import dataclass
from datetime import datetime
from typing import Any
import requests
@dataclass()
class User: # (1)
id: int
name: str
created_on: datetime
class UserClient:
"""Simple HTTP client for interacting with a user provider service."""
def __init__(self, hostname: str) -> None: # (2)
self._hostname = hostname
def get_user(self, user_id: int) -> User:
"""Get a user by ID from the provider."""
response = requests.get(f"{self._hostname}/users/{user_id}")
response.raise_for_status()
data: dict[str, Any] = response.json()
return User( # (3)
id=data["id"],
name=data["name"],
created_on=datetime.fromisoformat(data["created_on"]),
)
-
The
User
dataclass represents the user data model as used by the client. Importantly, this is not necessarily the same as the data model used by the provider. The Pact contract should reflect what the consumer needs, not what the provider actually implements. -
The initialiser for the
UserClient
class takes ahostname
parameter, which is the base URL of the user provider service. This ensures that the client can be easily pointed to the mock service during testing. -
Only the fields required by the consumer are included in the
User
dataclass. The provider might return additional fields (e.g.,email
,last_login
, etc.), but this consumer does not need to know about them and therefore they are ignored in the client implementation.
Consumer Test¶
The following is a Pact test for the UserClient
class defined above. It sets up a mock provider, defines the expected interactions, and verifies that the client behaves as expected.
from pathlib import Path
import pytest
from pact import Pact, match
@pytest.fixture
def pact() -> Generator[Pact, None, None]: # (1)
"""Set up a Pact mock provider for consumer tests."""
pact = Pact("user-consumer", "user-provider").with_specification("V4") # (2)
yield pact
pact.write_file(Path(__file__).parent / "pacts")
def test_get_user(pact: Pact) -> None:
"""Test the GET request for a user."""
response: dict[str, object] = { # (3)
"id": match.int(123),
"name": match.str("Alice"),
"created_on": match.datetime(),
}
(
pact.upon_receiving("A user request") # (4)
.given("the user exists", id=123, name="Alice") # (5)
.with_request("GET", "/users/123") # (6)
.will_respond_with(200) # (7)
.with_body(response, content_type="application/json") # (8)
)
with pact.serve() as srv: # (9)
client = UserClient(str(srv.url)) # (10)
user = client.get_user(123)
assert user.name == "Alice"
-
A Pytest fixture provides a reusable
pact
object for multiple tests. In this case, the fixture creates aPact
instance representing the contract between the consumer and provider. The fixture yields thepact
object to the test function, and after the test completes, writes the generated pact file to the specified directory. -
The Pact specification version is set to
"V4"
to ensure compatibility with the latest features and improvements in the Pact ecosystem. Note that this is the default version, so this line is optional unless you want to specify a different version. -
The expected response is defined using the
match
module for flexible matching of the response data. Here, theid
field is expected to be an integer, thename
field a string, and thecreated_on
field a datetime string. The specific values are not important, as long as they match the expected types. -
The
upon_receiving
method defines the description of the interaction. This description also uniquely identifies the interaction within the Pact file. -
The
given
method sets up the provider state, indicating that the user with ID 123 exists. Pact allows parameters to be passed to the provider state, which can be used to set up the provider in a specific way. Here, the parametersid=123
andname="Alice"
are provided, which the provider can use to create the user if necessary. -
The
with_request
method defines the expected request that the consumer will make. Here, it specifies that aGET
request will be made to the/users/123
endpoint. -
The
will_respond_with
method specifies the expected HTTP status code of the response. Here, a200 OK
status is expected. Thewill_respond_with
method also helps separate the request definition from the response definition, improving readability. -
The
with_body
method defines the expected body of the response, using theresponse
dictionary defined earlier. Thecontent_type
parameter specifies that the response will be in JSON format. -
The
pact.serve()
method starts the mock service, and thesrv
object provides the URL of the mock service. Within this context, any requests made to the mock service will be handled according to the interactions defined on thepact
object. Once the context is exited, the mock service is stopped, and the interactions are verified to ensure all expected requests were made. -
The
UserClient
is instantiated with this URL, and theget_user
method is called to retrieve the user data. The test asserts that the returned user's name is "Alice".
The test begins with a Pytest fixture that creates a reusable Pact instance representing the contract between "user-consumer"
and "user-provider"
. The expected response is defined using flexible matchers (match.int()
, match.str()
, match.datetime()
) to validate data types rather than exact values, making the test more robust against varying response data.
The interaction definition includes a description, provider state parameters, request details, and expected response format. Only the required parts of the interaction are specified, rather than an exhaustive specification. For example, the client will typically add additional headers (e.g., User-Agent
, Accept
, etc.) to the request, but these are not necessary for the contract and are therefore omitted. Similarly, the provider's response may include additional fields or headers that the consumer will ignore, so these are also not included in the contract.
The pact.serve()
context manager starts a mock provider service that handles requests according to the defined interactions, creating a controlled testing environment. The actual client code is then executed against this mock service, ensuring it makes correct requests and handles responses properly. Once the context is exited, the Pact file is automatically written to the specified directory for later provider verification, completing the consumer-driven contract testing cycle.
Warning
A common mistake is to use a generic HTTP client (e.g., requests
, httpx
, etc.) to make requests to the mock service within the test. This defeats the purpose of the test, as it does not verify that the client is making the correct requests and handling the responses correctly.
Multi-Interaction Testing¶
The mock service can handle multiple interactions within a single test. This is useful when you want to test a sequence of requests and responses. For example, a first request might create a background task, a second request might check the status of that task, and a final request retrieves the result. This flow can be tested in a single test function by defining multiple interactions on the pact
object:
(
pact.upon_receiving("A request to create a task")
.with_request("POST", "/tasks", body={"type": "long_running"})
.will_respond_with(202)
.with_header("Location", "/tasks/1/status")
)
(
pact.upon_receiving("A request to check task status")
.with_request("GET", "/tasks/1/status")
.will_respond_with(200)
.with_body({"status": "completed"})
.with_headers({
"Task-ID": "1",
"Location": "/tasks/1/result",
})
)
(
pact.upon_receiving("A request to get task result")
.with_request("GET", "/tasks/1/result")
.will_respond_with(200)
.with_body({"result": "Task completed successfully"})
)
When the mock service is started with pact.serve()
, it will handle requests for all defined interactions, ensuring the client code can be tested against a realistic sequence of operations. Furthermore, for the test to pass, all defined interactions must be exercised by the client code. If any interaction is not used, the test will fail.
Mock Service¶
Pact provides a mock service that simulates the provider service based on the defined interactions. The mock service is started when the pact
object is used as a context manager with pact.serve()
, as shown in the consumer test example above.
The mock service automatically selects a random free port by default, helping to avoid port conflicts when running multiple tests. You can optionally specify a custom host and port during Pact creation if needed for your testing environment.
with pact.serve(host="localhost", port=1234) as srv:
client = UserClient(str(srv.url))
user = client.get_user(123)
The mock service offers several important features when building your contracts:
- It provides a real HTTP server that your code can contact during the test and returns the responses you defined.
- You provide the expectations for the requests your code will make, and it asserts the contents of the actual requests made against your expectations.
- If a request is made that does not match one you defined, or if a request from your code is missing, it returns an error with details.
Broker¶
The above example showed how to test a single consumer; however, without also testing the provider, the test is incomplete. The Pact Broker is a service that allows you to share and manage your contracts between your consumer and provider tests. It acts as a central repository for your contracts, allowing you to publish contracts from your consumer tests and retrieve them in your provider tests.
Once the tests are complete (and successful), the contracts can be uploaded to the Pact Broker. The provider can then download the contracts and run its own tests to ensure it meets the consumer's expectations.
The Broker CLI is a command-line tool that can be installed through the pact-python-cli
package, or directly from the Pact Standalone releases page. It bundles several standalone CLI tools, including the pact-broker
CLI client.
The general syntax for the CLI is:
pact-broker publish \
/path/to/pacts/consumer-provider.json \
--consumer-app-version 1.0.0 \
--auto-detect-version-properties
It expects the following environment variables to be set:
PACT_BROKER_BASE_URL
-
The base URL of the Pact Broker (e.g.,
https://test.pactflow.io
if using PactFlow, or the URL to your self-hosted Pact Broker instance). PACT_BROKER_USERNAME
/PACT_BROKER_PASSWORD
-
The username and password for authenticating with the Pact Broker.
PACT_BROKER_TOKEN
-
An alternative to using username and password, this is a token that can be used for authentication (e.g., used with PactFlow).
Pattern Matching¶
Simple equality checks work for basic scenarios, but realistic tests need flexible matching to handle variable data such as timestamps, IDs, and dynamic content. The match
module provides matchers that validate data structure and types rather than exact values.
from pact import match
# Instead of exact matches that break easily:
response = {
"id": 12345, # Brittle - specific value
"email": "user@example.com", # Fails if email changes
"created_at": "2024-01-15T10:30:00Z" # Breaks on different timestamps
}
# Use flexible matchers:
response = {
"id": match.int(12345), # Any integer
"email": match.regex("user@example.com", regex=r".+@.+\..+"),
"created_at": match.datetime("2024-01-15T10:30:00Z")
}
Common matcher types include:
- Type matchers:
match.int()
,match.str()
,match.bool()
- validate data types - Pattern matchers:
match.regex()
,match.uuid()
- validate specific formats - Collection matchers:
match.each_like()
,match.array_containing()
- handle arrays and objects - Date/time matchers:
match.date()
,match.time()
,match.datetime()
- flexible timestamp handling
Matchers ensure your contracts focus on data structure and semantics rather than brittle exact values, making tests more robust and maintainable.
For comprehensive documentation and examples, see the API Reference and the match
module documentation. For more about Pact's matching specification, see Matching.
Dynamic Data Generation¶
While matchers validate that received data conforms to expected patterns, generators produce realistic test data for responses. The generate
module provides functions to create dynamic values that change on each test run, making your Pact contracts more realistic and robust.
from pact import generate
# Instead of static values in your mock responses
response = {
"user_id": 123, # Always the same
"session_token": "abc-def-123", # Predictable
"created_at": "2024-07-20T14:30:00+00:00" # Never changes
}
# Use generators for dynamic, realistic data
response = {
"user_id": generate.int(min=1, max=999999),
"session_token": generate.uuid(),
"created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z")
}
Generators are particularly useful when:
- Testing with fresh data: Each test run uses different values, helping catch issues with data handling
- Avoiding test pollution: Dynamic IDs and tokens prevent tests from accidentally depending on specific values
- Simulating real conditions: Generated timestamps, UUIDs, and random numbers better represent actual API behavior
- Provider state integration: Using
generate.provider_state()
to inject values from the provider's test setup
Common Generators¶
from pact import generate
response = {
# Numeric values with constraints
"user_id": generate.int(min=1, max=999999),
"price": generate.float(precision=2), # 2 total digits
"hex_color": generate.hex(digits=6), # 6-digit hex code
# String and text data
"username": generate.str(size=8), # 8-character string
"confirmation": generate.regex(r"[A-Z]{3}-\d{4}"), # Pattern-based
# Identifiers
"session_id": generate.uuid(), # Standard UUID format
"simple_id": generate.uuid(format="simple"), # No hyphens
# Dates and times
"created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z"),
"birth_date": generate.date("%Y-%m-%d"),
"start_time": generate.time("%H:%M:%S"),
# Boolean values
"is_active": generate.bool(),
# Provider-specific values
"server_url": generate.mock_server_url(),
"dynamic_value": generate.provider_state("${expression}")
}
Combining Matchers and Generators¶
Matchers and generators work together to create flexible, realistic contracts. Use matchers to validate incoming data and generators to produce dynamic response data:
# Request validation with matchers
request_body = {
"email": match.regex("user@example.com", regex=r".+@.+\..+"),
"age": match.int(25, min=18, max=100),
"preferences": match.array_containing([match.str("notifications")])
}
# Response generation with dynamic data
response_body = {
"id": generate.int(min=100000, max=999999),
"email": match.str("user@example.com"), # Echo back the input
"verification_token": generate.uuid(), # Fresh token each time
"created_at": generate.datetime("%Y-%m-%dT%H:%M:%S%z"),
"profile_url": generate.mock_server_url(
example="/profiles/12345",
regex=r"/profiles/\d+"
)
}
This approach ensures your tests validate the correct data structures while generating realistic, varied response data that better simulates real-world API behaviour.