Skip to content

Pact

Pact between a consumer and a provider.

This module defines the classes that are used to define a Pact between a consumer and a provider. It defines the interactions between the two parties, and provides the functionality to verify that the interactions are satisfied.

As Pact is a consumer-driven contract testing tool, the consumer is responsible for defining the interactions between the two parties. The provider is then responsible for ensuring that these interactions are satisfied.

Usage

The main class in this module is the Pact class. This class defines the Pact between the consumer and the provider. It is responsible for defining the interactions between the two parties.

The general usage of this module is as follows:

from pact.v3 import Pact
import aiohttp


pact = Pact("consumer", "provider")

interaction = pact.upon_receiving("a basic request")
interaction.given("user 123 exists")
interaction.with_request("GET", "/user/123")
interaction.will_respond_with(200)
interaction.with_header("Content-Type", "application/json")
interaction.with_body({"id": 123, "name": "Alice"})

with pact.serve() as srv:
    async with aiohttp.ClientSession(srv.url) as session:
        async with session.get("/user/123") as resp:
            assert resp.status == 200
            assert resp.headers["Content-Type"] == "application/json"
            data = await resp.json()
            assert data == {"id": 123, "name": "Alice"}

The repeated calls to interaction can be chained together to define the interaction in a more concise manner:

pact = Pact("consumer", "provider")

(
    pact.upon_receiving("a basic request")
    .given("user 123 exists")
    .with_request("GET", "/user/123")
    .will_respond_with(200)
    .with_header("Content-Type", "application/json")
    .with_body({"id": 123, "name": "Alice"})
)

Note that the parentheses are required to ensure that the method chaining works correctly, as this form of method chaining is not typical in Python.

Attributes

logger = logging.getLogger(__name__) module-attribute

Classes

MismatchesError(mismatches: list[dict[str, Any]])

Bases: Exception

Exception raised when there are mismatches between the Pact and the server.

PARAMETER DESCRIPTION
mismatches

Mismatches between the Pact and the server.

TYPE: list[dict[str, Any]]

Source code in src/pact/v3/pact.py
def __init__(self, mismatches: list[dict[str, Any]]) -> None:
    """
    Initialise a new MismatchesError.

    Args:
        mismatches:
            Mismatches between the Pact and the server.
    """
    super().__init__(f"Mismatched interaction (count: {len(mismatches)})")
    self._mismatches = mismatches

Attributes

mismatches: list[dict[str, Any]] property

Mismatches between the Pact and the server.

Pact(consumer: str, provider: str)

A Pact between a consumer and a provider.

This class defines a Pact between a consumer and a provider. It is the central class in Pact's framework, and is responsible for defining the interactions between the two parties.

One Pact instance should be created for each provider that a consumer interacts with. The methods on this class are used to define the broader attributes of the Pact, such as the consumer and provider names, the Pact specification, any plugins that are used, and any metadata that is attached to the Pact.

Each interaction between the consumer and the provider is defined through the upon_receiving method, which returns a sub-class of Interaction.

PARAMETER DESCRIPTION
consumer

Name of the consumer.

TYPE: str

provider

Name of the provider.

TYPE: str

Source code in src/pact/v3/pact.py
def __init__(
    self,
    consumer: str,
    provider: str,
) -> None:
    """
    Initialise a new Pact.

    Args:
        consumer:
            Name of the consumer.

        provider:
            Name of the provider.
    """
    if not consumer:
        msg = "Consumer name cannot be empty."
        raise ValueError(msg)
    if not provider:
        msg = "Provider name cannot be empty."
        raise ValueError(msg)

    self._consumer = consumer
    self._provider = provider
    self._interactions: Set[Interaction] = set()
    self._handle: pact.v3.ffi.PactHandle = pact.v3.ffi.new_pact(
        consumer,
        provider,
    )

Attributes

consumer: str property

Consumer name.

provider: str property

Provider name.

specification: pact.v3.ffi.PactSpecification property

Pact specification version.

Functions

interactions(kind: str = 'HTTP') -> pact.v3.ffi.PactSyncHttpIterator | pact.v3.ffi.PactSyncMessageIterator | pact.v3.ffi.PactMessageIterator

Return an iterator over the Pact's interactions.

The kind is used to specify the type of interactions that will be iterated over.

Source code in src/pact/v3/pact.py
def interactions(
    self,
    kind: str = "HTTP",
) -> (
    pact.v3.ffi.PactSyncHttpIterator
    | pact.v3.ffi.PactSyncMessageIterator
    | pact.v3.ffi.PactMessageIterator
):
    """
    Return an iterator over the Pact's interactions.

    The kind is used to specify the type of interactions that will be
    iterated over.
    """
    # TODO: Add an iterator for `All` interactions.
    # https://github.com/pact-foundation/pact-python/issues/451
    if kind == "HTTP":
        return pact.v3.ffi.pact_handle_get_sync_http_iter(self._handle)
    if kind == "Sync":
        return pact.v3.ffi.pact_handle_get_sync_message_iter(self._handle)
    if kind == "Async":
        return pact.v3.ffi.pact_handle_get_message_iter(self._handle)
    msg = f"Unknown interaction type: {kind}"
    raise ValueError(msg)
messages() -> pact.v3.ffi.PactMessageIterator

Iterate over the messages in the Pact.

This function returns an iterator over the messages in the Pact. This is useful for validating the Pact against the provider.

pact = Pact("consumer", "provider")
with pact.serve() as srv:
    for message in pact.messages():
        # Validate the message against the provider.
        ...

Note that the Pact must be written to a file before the messages can be iterated over. This is because the messages are not stored in memory, but rather are streamed directly from the file.

Source code in src/pact/v3/pact.py
def messages(self) -> pact.v3.ffi.PactMessageIterator:
    """
    Iterate over the messages in the Pact.

    This function returns an iterator over the messages in the Pact. This
    is useful for validating the Pact against the provider.

    ```python
    pact = Pact("consumer", "provider")
    with pact.serve() as srv:
        for message in pact.messages():
            # Validate the message against the provider.
            ...
    ```

    Note that the Pact must be written to a file before the messages can be
    iterated over. This is because the messages are not stored in memory,
    but rather are streamed directly from the file.
    """
    return pact.v3.ffi.pact_handle_get_message_iter(self._handle)
serve(addr: str = 'localhost', port: int = 0, transport: str = 'http', transport_config: str | None = None, *, raises: bool = True, verbose: bool = True) -> PactServer

Return a mock server for the Pact.

This function configures a mock server for the Pact. The mock server is then started when the Pact is entered into a with block:

pact = Pact("consumer", "provider")
with pact.serve() as srv:
    ...
PARAMETER DESCRIPTION
addr

Address to bind the mock server to. Defaults to localhost.

TYPE: str DEFAULT: 'localhost'

port

Port to bind the mock server to. Defaults to 0, which will select a random port.

TYPE: int DEFAULT: 0

transport

Transport to use for the mock server. Defaults to HTTP.

TYPE: str DEFAULT: 'http'

transport_config

Configuration for the transport. This is specific to the transport being used and should be a JSON string.

TYPE: str | None DEFAULT: None

raises

Whether to raise an exception if there are mismatches between the Pact and the server. If set to False, then the mismatches must be handled manually.

TYPE: bool DEFAULT: True

verbose

Whether or not to print the mismatches to the logger. This works independently of raises.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
PactServer

A PactServer instance.

Source code in src/pact/v3/pact.py
def serve(  # noqa: PLR0913
    self,
    addr: str = "localhost",
    port: int = 0,
    transport: str = "http",
    transport_config: str | None = None,
    *,
    raises: bool = True,
    verbose: bool = True,
) -> PactServer:
    """
    Return a mock server for the Pact.

    This function configures a mock server for the Pact. The mock server
    is then started when the Pact is entered into a `with` block:

    ```python
    pact = Pact("consumer", "provider")
    with pact.serve() as srv:
        ...
    ```

    Args:
        addr:
            Address to bind the mock server to. Defaults to `localhost`.

        port:
            Port to bind the mock server to. Defaults to `0`, which will
            select a random port.

        transport:
            Transport to use for the mock server. Defaults to `HTTP`.

        transport_config:
            Configuration for the transport. This is specific to the
            transport being used and should be a JSON string.

        raises:
            Whether to raise an exception if there are mismatches between
            the Pact and the server. If set to `False`, then the mismatches
            must be handled manually.

        verbose:
            Whether or not to print the mismatches to the logger. This works
            independently of `raises`.

    Returns:
        A [`PactServer`][pact.v3.pact.PactServer] instance.
    """
    return PactServer(
        self._handle,
        addr,
        port,
        transport,
        transport_config,
        raises=raises,
        verbose=verbose,
    )
upon_receiving(description: str, interaction: Literal['HTTP', 'Sync', 'Async'] = 'HTTP') -> HttpInteraction | AsyncMessageInteraction | SyncMessageInteraction

Create a new Interaction.

PARAMETER DESCRIPTION
description

Description of the interaction. This must be unique within the Pact.

TYPE: str

interaction

Type of interaction. Defaults to HTTP. This must be one of HTTP, Async, or Sync.

TYPE: Literal['HTTP', 'Sync', 'Async'] DEFAULT: 'HTTP'

Source code in src/pact/v3/pact.py
def upon_receiving(
    self,
    description: str,
    interaction: Literal["HTTP", "Sync", "Async"] = "HTTP",
) -> HttpInteraction | AsyncMessageInteraction | SyncMessageInteraction:
    """
    Create a new Interaction.

    Args:
        description:
            Description of the interaction. This must be unique
            within the Pact.

        interaction:
            Type of interaction. Defaults to `HTTP`. This must be one of
            `HTTP`, `Async`, or `Sync`.
    """
    if interaction == "HTTP":
        return HttpInteraction(self._handle, description)
    if interaction == "Async":
        return AsyncMessageInteraction(self._handle, description)
    if interaction == "Sync":
        return SyncMessageInteraction(self._handle, description)

    msg = f"Invalid interaction type: {interaction}"
    raise ValueError(msg)
using_plugin(name: str, version: str | None = None) -> Self

Add a plugin to be used by the test.

Plugins extend the functionality of Pact.

PARAMETER DESCRIPTION
name

Name of the plugin.

TYPE: str

version

Version of the plugin. This is optional and can be None.

TYPE: str | None DEFAULT: None

Source code in src/pact/v3/pact.py
def using_plugin(self, name: str, version: str | None = None) -> Self:
    """
    Add a plugin to be used by the test.

    Plugins extend the functionality of Pact.

    Args:
        name:
            Name of the plugin.

        version:
            Version of the plugin. This is optional and can be `None`.
    """
    pact.v3.ffi.using_plugin(self._handle, name, version)
    return self
with_metadata(namespace: str, metadata: dict[str, str]) -> Self

Set additional metadata for the Pact.

A common use for this function is to add information about the client library (name, version, hash, etc.) to the Pact.

PARAMETER DESCRIPTION
namespace

Namespace for the metadata. This is used to group the metadata together.

TYPE: str

metadata

Key-value pairs of metadata to add to the Pact.

TYPE: dict[str, str]

Source code in src/pact/v3/pact.py
def with_metadata(
    self,
    namespace: str,
    metadata: dict[str, str],
) -> Self:
    """
    Set additional metadata for the Pact.

    A common use for this function is to add information about the client
    library (name, version, hash, etc.) to the Pact.

    Args:
        namespace:
            Namespace for the metadata. This is used to group the metadata
            together.

        metadata:
            Key-value pairs of metadata to add to the Pact.
    """
    for k, v in metadata.items():
        pact.v3.ffi.with_pact_metadata(self._handle, namespace, k, v)
    return self
with_specification(version: str | pact.v3.ffi.PactSpecification) -> Self

Set the Pact specification version.

The Pact specification version indicates the features which are supported by the Pact, and certain default behaviours.

PARAMETER DESCRIPTION
version

Pact specification version. The can be either a string or a PactSpecification instance.

The version string is case insensitive and has an optional v prefix.

TYPE: str | PactSpecification

Source code in src/pact/v3/pact.py
def with_specification(
    self,
    version: str | pact.v3.ffi.PactSpecification,
) -> Self:
    """
    Set the Pact specification version.

    The Pact specification version indicates the features which are
    supported by the Pact, and certain default behaviours.

    Args:
        version:
            Pact specification version. The can be either a string or a
            [`PactSpecification`][pact.v3.ffi.PactSpecification] instance.

            The version string is case insensitive and has an optional `v`
            prefix.
    """
    if isinstance(version, str):
        version = pact.v3.ffi.PactSpecification.from_str(version)
    pact.v3.ffi.with_specification(self._handle, version)
    return self
write_file(directory: Path | str | None = None, *, overwrite: bool = False) -> None

Write out the pact to a file.

This function should be called once all of the consumer tests have been run. It writes the Pact to a file, which can then be used to validate the provider.

PARAMETER DESCRIPTION
directory

The directory to write the pact to. If the directory does not exist, it will be created. The filename will be automatically generated from the underlying Pact.

TYPE: Path | str | None DEFAULT: None

overwrite

If set to True, the file will be overwritten if it already exists. Otherwise, the contents of the file will be merged with the existing file.

TYPE: bool DEFAULT: False

Source code in src/pact/v3/pact.py
def write_file(
    self,
    directory: Path | str | None = None,
    *,
    overwrite: bool = False,
) -> None:
    """
    Write out the pact to a file.

    This function should be called once all of the consumer tests have been
    run. It writes the Pact to a file, which can then be used to validate
    the provider.

    Args:
        directory:
            The directory to write the pact to. If the directory does not
            exist, it will be created. The filename will be
            automatically generated from the underlying Pact.

        overwrite:
            If set to True, the file will be overwritten if it already
            exists. Otherwise, the contents of the file will be merged with
            the existing file.
    """
    if directory is None:
        directory = Path.cwd()
    pact.v3.ffi.pact_handle_write_file(
        self._handle,
        directory,
        overwrite=overwrite,
    )

PactServer(pact_handle: pact.v3.ffi.PactHandle, host: str = 'localhost', port: int = 0, transport: str = 'HTTP', transport_config: str | None = None, *, raises: bool = True, verbose: bool = True)

Pact Server.

This class handles the lifecycle of the Pact mock server. It is responsible for starting the mock server when the Pact is entered into a with block, and stopping the mock server when the block is exited.

Note that the server should not be started directly, but rather through the serve(...) method of a Pact:

pact = Pact("consumer", "provider")
# Define interactions...
with pact.serve() as srv:
    ...

The URL for the server can be accessed through its url attribute, which will be required in order to point the consumer client to the mock server:

pact = Pact("consumer", "provider")
with pact.serve() as srv:
    api_client = MyApiClient(srv.url)
    # Test the client...

If the server is instantiated with raises=True (the default), then the server will raise a MismatchesError if there are mismatches in any of the interactions. If raises=False, then the mismatches must be handled manually.

PARAMETER DESCRIPTION
pact_handle

Handle for the Pact.

TYPE: PactHandle

host

Hostname of IP for the mock server.

TYPE: str DEFAULT: 'localhost'

port

Port to bind the mock server to. The value of 0 will select a random available port.

TYPE: int DEFAULT: 0

transport

Transport to use for the mock server.

TYPE: str DEFAULT: 'HTTP'

transport_config

Configuration for the transport. This is specific to the transport being used and should be a JSON string.

TYPE: str | None DEFAULT: None

raises

Whether or not to raise an exception if the server is not matched upon exit.

TYPE: bool DEFAULT: True

verbose

Whether or not to print the mismatches to the logger. This works independently of raises.

TYPE: bool DEFAULT: True

Source code in src/pact/v3/pact.py
def __init__(  # noqa: PLR0913
    self,
    pact_handle: pact.v3.ffi.PactHandle,
    host: str = "localhost",
    port: int = 0,
    transport: str = "HTTP",
    transport_config: str | None = None,
    *,
    raises: bool = True,
    verbose: bool = True,
) -> None:
    """
    Initialise a new Pact Server.

    Args:
        pact_handle:
            Handle for the Pact.

        host:
            Hostname of IP for the mock server.

        port:
            Port to bind the mock server to. The value of `0` will select a
            random available port.

        transport:
            Transport to use for the mock server.

        transport_config:
            Configuration for the transport. This is specific to the
            transport being used and should be a JSON string.

        raises:
            Whether or not to raise an exception if the server is not
            matched upon exit.

        verbose:
            Whether or not to print the mismatches to the logger. This works
            independently of `raises`.
    """
    self._host = host
    self._port = port
    self._transport = transport
    self._transport_config = transport_config
    self._pact_handle = pact_handle
    self._handle: None | pact.v3.ffi.PactServerHandle = None
    self._raises = raises
    self._verbose = verbose

Attributes

host: str property

Address to which the server is bound.

logs: str | None property

Logs from the server.

This is a string containing the logs from the server. If there are no logs, then this is None. For this to be populated, the logging must be configured to make use of the internal buffer.

RAISES DESCRIPTION
RuntimeError

If the server is not running.

matched: bool property

Whether or not the server has been matched.

This is True if the server has been matched, and False otherwise.

RAISES DESCRIPTION
RuntimeError

If the server is not running.

mismatches: list[dict[str, Any]] property

Mismatches between the Pact and the server.

This is a string containing the mismatches between the Pact and the server. If there are no mismatches, then this is an empty string.

RAISES DESCRIPTION
RuntimeError

If the server is not running.

port: int property

Port on which the server is running.

If the server is not running, then this will be 0.

transport: str property

Transport method.

url: URL property

Base URL for the server.

Functions

write_file(directory: str | Path | None = None, *, overwrite: bool = False) -> None

Write out the pact to a file.

PARAMETER DESCRIPTION
directory

The directory to write the pact to. If the directory does not exist, it will be created. The filename will be automatically generated from the underlying Pact.

TYPE: str | Path | None DEFAULT: None

overwrite

Whether or not to overwrite the file if it already exists.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
RuntimeError

If the server is not running.

ValueError

If the path specified is not a directory.

Source code in src/pact/v3/pact.py
def write_file(
    self,
    directory: str | Path | None = None,
    *,
    overwrite: bool = False,
) -> None:
    """
    Write out the pact to a file.

    Args:
        directory:
            The directory to write the pact to. If the directory does not
            exist, it will be created. The filename will be
            automatically generated from the underlying Pact.

        overwrite:
            Whether or not to overwrite the file if it already exists.

    Raises:
        RuntimeError:
            If the server is not running.

        ValueError:
            If the path specified is not a directory.
    """
    if not self._handle:
        msg = "The server is not running."
        raise RuntimeError(msg)

    directory = Path(directory) if directory else Path.cwd()
    if not directory.exists():
        directory.mkdir(parents=True)
    elif not directory.is_dir():
        msg = f"{directory} is not a directory"
        raise ValueError(msg)

    pact.v3.ffi.write_pact_file(
        self._handle,
        str(directory),
        overwrite=overwrite,
    )