Migration Guide¶
[!INFO]
This document is best viewed on the Pact Python docs site.
This document outlines the key changes and migration steps for users transitioning between major Pact Python versions. It focuses on breaking changes, new features, and best practices to ensure a smooth upgrade.
Migrating from 3.x to 4.x¶
Key Changes¶
- The
pact.v2compatibility module and thecompat-v2extra have been removed, along with thepact-verifierconsole script. Code still importing frompact.v2must move to the3.xAPI described in Migrating from2.xto3.x. - Python 3.11 or newer is required.
- The
compat-v2extra used to installpact-python-cli. Install it directly if you rely on the Pact CLI (for example,pact brokerto publish contracts).
Projects that cannot migrate yet should pin pact-python<4, which still provides pact.v2 through pact-python[compat-v2].
Migrating pact-python-cli from 2.x to 1!x¶
pact-python-cli is now published from pact-foundation/pact-cli and ships its Rust pact binary. Its versions follow that project under PEP 440 epoch 1 (1!0.10.10, for example), so they sort above the 2.x releases.
Key Changes¶
-
The Ruby standalone tools are gone. Each command maps to a
pactsubcommand:2.xcommand1!xcommandpact-brokerpact brokerpactflowpact pactflowpact-mock-service,pact_mock_server_clipact mockpact-provider-verifier,pact_verifier_clipact verifierpact-stub-service,pact-stub-serverpact stubpact-plugin-clipact pluginpact-messageNo equivalent -
The
pact_cliPython module is gone, along withPACT_USE_SYSTEM_BINSand the*_PATHconstants. Find the binary withshutil.which("pact"). - Linux with glibc older than 2.34 has no
1!xwheel, so pip installs2.6.4.0there. Installpactwith the Pact CLI installers instead.
Projects that still need the Ruby tools should pin pact-python-cli<1!0.
Migrating from 2.x to 3.x¶
Key Changes¶
- Replaced the entire Pact CLI-based implementation with a brand new version leveraging the core Pact FFI written in Rust.
- 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).
- The bundled CLI is now a separate package:
pact-python-cli. - The programmatic API has been completely overhauled to be more Pythonic and easier to use. Legacy process calls and return code checks have been removed in favour of proper exception handling.
- The old programmatic interface was available in the
pact.v2module during the3.xseries. It was removed in4.0.0; see Migrating from3.xto4.x.
Consumer Changes¶
The v3 API introduces significant changes to how consumer tests are structured and written. The main changes simplify the API, making it more Pythonic and flexible.
Defining a Pact¶
The Consumer and Provider classes have been removed. Instead, a single Pact class is used to define the consumer-provider relationship:
from pact.v2 import Consumer, Provider
consumer = Consumer("my-web-front-end")
provider = Provider("my-backend-service")
pact = consumer.has_pact_with(provider, pact_dir="/path/to/pacts")
Defining Interactions¶
The v3 interface favours method chaining and provides more granular control over request and response definitions.
(
pact
.given("user exists")
.upon_receiving("a request for user data")
.with_request(
"GET",
"/users/123",
headers={"Accept": "application/json"},
query={"include": "profile"},
)
.will_respond_with(
200,
headers={"Content-Type": "application/json"},
body={"id": 123, "name": "Alice"},
)
)
(
pact
.upon_receiving("a request for user data")
.given("user exists", id=123, name="Alice") # (1)
.with_request("GET", "/users/123")
.with_header("Accept", "application/json")
.with_query_parameter("include", "profile")
.will_respond_with(200)
.with_header("Content-Type", "application/json")
.with_body({"id": 123, "name": "Alice"}, content_type="application/json")
)
-
In v2, there was limited support for parameterizing provider states, and each state variation often required a separate definition. For example,
given("user Alice exists with id 123")andgiven("user Bob exists with id 456")would be two distinct states, which would then need to be handled separately in the provider state setup.The new interface can now define a common descriptor that can be reused with different parameters:
.given("user exists", id=123, name='Alice')and.given("user exists", id=456, name='Bob'). This approach reduces redundancy and makes it easier to manage provider states.
Some methods are shared across request and response definitions, such as with_header() and with_body(). Pact Python automatically applies them to the correct part of the interaction based on whether they are called before or after will_respond_with(). Alternatively, these methods accept an optional part argument to explicitly specify whether they apply to the request or response.
Running Tests¶
Pact Python v2 had two different ways to run consumer tests, both of which spawned a separate mock service process. The new v3 API provides a single, consistent way to run tests using the serve() method.
pact = Consumer("my-consumer").has_pact_with(
Provider("my-provider"),
host_name="localhost",
port=1234,
)
# Context manager automatically calls setup() and verify()
with pact:
response = requests.get(pact.uri + "/users/123")
# Pact file written automatically on exit
pact = Consumer("my-consumer").has_pact_with(
Provider("my-provider"),
host_name="localhost",
port=1234,
)
# Manually start the mock service
pact.start_service()
pact.setup() # Configure interactions
# Make requests
response = requests.get(pact.uri + "/users/123")
# Assertions...
# Verify and stop
pact.verify() # Writes pact file
pact.stop_service()
The new API entirely replaces both of these approaches with a single, consistent method:
pact = Pact("my-consumer", "my-provider")
with pact.serve() as srv:
response = requests.get(f"{srv.url}/users/123")
The server host and port can be specified in serve() if needed, but by default, the server binds to localhost on a random available port. More details can be found in the API reference.
Writing Pact Files¶
Since the old v2 API executed a sub-process for the mock service, the Pact file was automatically written when the context manager exited or when pact.verify() was called. The new v3 API runs the mock service in-process, so the Pact file must be written explicitly using the write_file() method:
pact = Pact("consumer", "provider")
# Define interactions and run tests...
pact.write_file("/path/to/pacts")
Matchers¶
Support for matchers has been greatly expanded and improved in the v3 API. The older v2 classes defined a limited set of matchers, while the new API provides a more comprehensive and flexible approach.
from pact.v2.matchers import Like, EachLike, Regex, Term
# Usage:
Like({"id": 123})
EachLike({"item": "value"})
Regex("hello world", r"^hello")
from pact import match
# Usage:
match.like({"id": 123})
match.each_like({"item": "value"})
match.regex("hello world", r"^hello")
For a full list of available matchers and their usage, refer to the API documentation.
Provider Changes¶
The provider verification API has been completely redesigned in v3 to provide a more intuitive and flexible interface. The old Provider and Verifier classes have been replaced by a single Verifier class with a fluent interface.
Creating a Verifier¶
from pact.v2 import Provider, Verifier
# Create separate Provider and Verifier instances
provider = Provider("my-provider")
verifier = Verifier(provider, "http://localhost:8080")
from pact import Verifier
# Single Verifier instance with provider name
verifier = Verifier("my-provider")
The protocol specification is now done through the add_transport method, which allows for more flexible configuration and supports multiple transports if needed.
verifier = (
Verifier("my-provider")
.add_transport(url="http://localhost:8080")
# Or more granular control:
.add_transport(protocol="http", port=8080, path="/api/v1", scheme="https")
)
Adding Pact Sources¶
Support for both local files and Pact Brokers is retained in v3, with the verify_pacts and verify_with_broker methods replaced by a more flexible source configuration. This allows multiple sources to be combined, and selectors to be applied.
success, logs = verifier.verify_with_broker(
broker_url="https://pact-broker.example.com",
broker_username="username",
broker_password="password",
)
verifier = Verifier("my-provider").broker_source(
"https://pact-broker.example.com", username="username", password="password"
)
# Or with selectors for more control
broker_builder = (
verifier
.broker_source("https://pact-broker.example.com", selector=True)
.include_pending()
.provider_branch("main")
.consumer_version(branch="main")
.consumer_version(branch="develop")
.build()
)
The selector=True argument returns a BrokerSelectorBuilder instance, which provides methods to configure which pacts to fetch. The build() call finalizes the configuration and returns the Verifier instance which can then be further configured.
The consumer_version method provides fine-grained control over which consumer pacts are verified and can be called multiple times to add multiple selectors (combined with a logical OR). The older consumer_versions method is now deprecated in favor of consumer_version.
Provider State Handling¶
The old v2 API required the provider to expose an HTTP endpoint dedicated to handling provider states. This is still supported in v3, but there are now more flexible options, allowing Python functions (or mappings of state names to functions) to be used instead.
success, logs = verifier.verify_pacts(
"./pacts/consumer-provider.json",
provider_states_setup_url="http://localhost:8080/_pact/provider_states",
)
# Option 1: URL-based (similar to v2)
verifier = (
Verifier("my-provider")
.add_transport(url="http://localhost:8080")
.state_handler(
"http://localhost:8080/_pact/provider_states",
body=True, # (1)
)
.add_source("./pacts/")
)
- The
bodyargument specifies whether to use aPOSTrequest and pass information in the body, or to use aGETrequest and pass information through HTTP headers. For more details, see thestate_handlerAPI documentation.
def handler(name, params=None):
if name == "user exists":
# Set up user in database/mock
create_user(params.get("id", 123))
elif name == "no users exist":
# Clear users
clear_users()
verifier = (
Verifier("my-provider")
.add_transport(url="http://localhost:8080")
.state_handler(handler)
.add_source("./pacts/")
)
state_handlers = {
"user exists": lambda name, params: create_user(params.get("id", 123)),
"no users exist": lambda name, params: clear_users(),
}
verifier = (
Verifier("my-provider")
.add_transport(url="http://localhost:8080")
.state_handler(state_handlers)
.add_source("./pacts/")
)
More information on the state handler function signature can be found in the state_handler API documentation. By default, the handlers only set up the provider state. If you need to also tear down the state after verification, you can use the teardown=True argument to enable this behaviour.
Warning
These functions run in the test process, so any side effects must be properly shared with the provider. If using mocking libraries, ensure the provider is started in a separate thread of the same process (using threading.Thread or similar), rather than a separate process (e.g., using multiprocessing.Process or subprocess.Popen).
Message Verification¶
Message verification is now much more straightforward in v3, with a a similar interface to HTTP verification and fixes a number of issues and deficiencies present in the v2 implementation (including the swapped behaviour of expects_to_receive and given, and the lack of support for matchers and generators).
def message_handler(description, metadata):
if description == "user created event":
return {"id": 123, "name": "Alice", "event": "created"}
elif description == "user deleted event":
return {"id": 123, "event": "deleted"}
verifier = (
Verifier("my-provider").message_handler(message_handler).add_source("./pacts/")
)
messages = {
"user created event": {"id": 123, "name": "Alice", "event": "created"},
"user deleted event": lambda desc, meta: {"id": 123, "event": "deleted"},
}
verifier = Verifier("my-provider").message_handler(messages).add_source("./pacts/")
Running Verification¶
Verification has been simplified and no longer requires checking return codes. Instead, the verify() method raises an exception on failure, or returns normally on success.