Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,16 @@ jobs:
set -o errexit
set -o pipefail

root="$(pwd)"

while IFS= read -r -d $'\0' file <&3; do
cd "$(dirname "$file")"
echo "Running example in $(pwd)"
example="${PWD#"$root"/}"

echo "::group::${example}"
uv run --python ${{ matrix.python-version }} --group test --with pytest-cov pytest --junit-xml=junit.xml --cov=pact --cov-report=xml:coverage-example.xml
done 3< <(find "$(pwd)/examples" -name pyproject.toml -print0)
echo "::endgroup::"
done 3< <(find "$root/examples" -name pyproject.toml -print0)

- name: Upload coverage
if: matrix.python-version == env.STABLE_PYTHON_VERSION && matrix.os == 'ubuntu-latest'
Expand Down
137 changes: 137 additions & 0 deletions examples/plugins/grpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Example: gRPC Contract Testing with the Protobuf Plugin

This example demonstrates contract testing for a true [gRPC](https://grpc.io/)
service using the [Pact protobuf
plugin](https://github.com/pactflow/pact-protobuf-plugin). Unlike the
[`protobuf`](../protobuf/) example, which exchanges Protocol Buffer messages over
plain HTTP, this example tests the `AddressBookService` gRPC service end-to-end
over the gRPC transport (HTTP/2).

It is designed to be pedagogical, highlighting how Pact's plugin system extends
contract testing beyond HTTP to alternative transports such as gRPC.

## Overview

- [**Proto definitions**][examples.plugins.proto]: The shared `person.proto`
service definition and its generated stubs, reused from the
[`protobuf`](../protobuf/) example.
- [**Consumer Tests**][examples.plugins.grpc.test_consumer]: Contract
definition and consumer testing against a mock gRPC server.
- [**Provider Tests**][examples.plugins.grpc.test_provider]: Provider
verification of a real gRPC server against the contract.

Use the above links to view additional documentation within.

## What This Example Demonstrates

### Consumer Side

- Describing a gRPC interaction to the protobuf plugin using a JSON document
that references the `.proto` file, the fully-qualified service method, and
the expected request and response messages.
- Standing up a mock gRPC server with `pact.serve(transport="grpc")`.
- Calling the mock server with a generated gRPC client stub.
- Writing the contract using the mock server (via `srv.write_file()`) so that
the gRPC transport is recorded in the pact.

### Provider Side

- Implementing the `AddressBookService` gRPC service using the shared
`person.proto` definition.
- Running the gRPC server in a background thread for verification.
- Verifying the provider with the Pact Verifier over the `grpc` transport.
- Provider state setup and teardown for isolated, repeatable verification.

## How gRPC Differs from the Protobuf Example

The [`protobuf`](../protobuf/) example sends protobuf-serialized messages as the
body of regular HTTP requests and responses. This example tests a real gRPC
service, where the protobuf plugin understands the service definition and:

- Stands up a mock gRPC server for the consumer test, responding to RPC calls
with the expected response message.
- Replays the consumer's recorded RPC calls against a real provider gRPC
server during verification.

The interaction is described to the plugin using the `application/grpc` content
type and a JSON document of the form:

```python
{
"pact:proto": "/path/to/person.proto",
"pact:proto-service": "AddressBookService/GetPerson",
"pact:content-type": "application/grpc",
"request": {"person_id": "matching(number, 1)"},
"response": {"person": {"name": "matching(type, 'Alice')", ...}},
}
```

The `matching(...)` expressions instruct Pact to verify the structure and types
of the messages rather than their exact values, which is the recommended
approach for contract testing.

## Prerequisites

- Python 3.10 or higher
- A dependency manager ([uv](https://docs.astral.sh/uv/) recommended,
[pip](https://pip.pypa.io/en/stable/) also works)
- The [Pact protobuf
plugin](https://github.com/pactflow/pact-protobuf-plugin), which provides
both protobuf content handling and the gRPC transport. Pact downloads the
plugin automatically the first time it is needed, so no manual step is
usually required. To pre-install it (e.g. for offline use), use the [Pact
plugin CLI](https://github.com/pact-foundation/pact-plugins/tree/main/cli):

```console
pact-plugin-cli install protobuf
```

## Running the Example

Run the tests from this directory, exactly like the other examples. Although the
tests reuse the shared proto definitions from the top-level `examples` package,
the example's `pyproject.toml` makes the repository root importable so they run
standalone.

### Using uv (Recommended)

We recommend using [uv](https://docs.astral.sh/uv/) to manage the virtual
environment and dependencies. The following command will automatically set up
the virtual environment, install dependencies, and execute the tests:

```console
uv run --group test pytest
```

### Using pip

If using the [`venv`][venv] module, the required steps are:

1. Create and activate a virtual environment:

```console
python -m venv .venv
source .venv/bin/activate # On macOS/Linux
.venv\Scripts\activate # On Windows
```

2. Install dependencies:

```console
pip install -U pip # Pip 25.1 is required
pip install --group test -e .
```

3. Run tests:

```console
pytest
```

## Related Documentation

- [Pact Documentation](https://docs.pact.io/)
- [Pact Protobuf/gRPC Plugin](https://github.com/pactflow/pact-protobuf-plugin)
- [gRPC Python Documentation](https://grpc.io/docs/languages/python/)
- [Protocol Buffers Documentation](https://protobuf.dev/)
- [pytest Documentation](https://docs.pytest.org/)
61 changes: 61 additions & 0 deletions examples/plugins/grpc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
gRPC Plugin Example for Pact Python v3.

This module provides an example of how to use Pact plugins to perform contract
testing over a non-HTTP transport. Specifically, this example demonstrates the
use of the protobuf plugin to test true gRPC interactions, where messages are
exchanged using Protocol Buffers over the gRPC transport (HTTP/2).

For detailed information about Protocol Buffers, the generated files, and the
domain model used in this example, see the [`proto`][examples.plugins.proto]
module documentation. The same `person.proto` definition (and its generated
`person_pb2` and `person_pb2_grpc` modules) is shared with the
[`protobuf`][examples.plugins.protobuf] example.

## gRPC and Pact

The [`protobuf`][examples.plugins.protobuf] example demonstrates how to test
interactions where protobuf messages are sent as the body of regular HTTP
requests and responses. This example goes one step further and tests a real
gRPC service.

gRPC builds on top of Protocol Buffers and HTTP/2 to provide a high-performance
remote procedure call (RPC) framework. Rather than sending protobuf messages as
opaque HTTP bodies, gRPC defines services with strongly-typed methods, each with
a request and response message. The Pact protobuf plugin understands this and is
able to:

- Stand up a mock gRPC server for the consumer test, responding to RPC calls
with the expected response message.
- Replay the consumer's RPC calls against a real provider gRPC server during
the provider verification.

## This Example

This example demonstrates how to use the Pact protobuf plugin to test a gRPC
service defined by the `AddressBookService` in `person.proto`. It is assumed
that you have a basic understanding of Pact, Protocol Buffers, and gRPC.

The sample data used throughout this example is shared with the protobuf example
through the [`address_book`][examples.plugins.protobuf.address_book] helper.
"""

from __future__ import annotations

from pathlib import Path

import examples.plugins.proto

PROTO_DIR = Path(examples.plugins.proto.__file__).parent
"""
Directory containing the shared `person.proto` definition.

The protobuf plugin requires the path to the `.proto` file in order to
understand the gRPC service and message definitions. This is shared with the
[`protobuf`][examples.plugins.protobuf] example rather than being duplicated.
"""

PROTO_FILE = PROTO_DIR / "person.proto"
"""
Path to the shared `person.proto` definition.
"""
33 changes: 33 additions & 0 deletions examples/plugins/grpc/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""
Shared pytest configuration for the gRPC plugin example.

Defines the `pacts_path` fixture used by both consumer and provider tests to
locate the directory where generated Pact contract files are stored.
"""

from __future__ import annotations

import contextlib
from pathlib import Path

import pytest

import pact_ffi

EXAMPLE_DIR = Path(__file__).parent.resolve()


@pytest.fixture(scope="session")
def pacts_path() -> Path:
"""Fixture providing the path to the generated Pact contract files."""
return EXAMPLE_DIR / "pacts"


@pytest.fixture(scope="session", autouse=True)
def _setup_pact_logging() -> None:
"""
Set up logging for the pact package.
"""
# If the logger is already configured, this will raise a RuntimeError.
with contextlib.suppress(RuntimeError):
pact_ffi.log_to_stderr("INFO")
32 changes: 32 additions & 0 deletions examples/plugins/grpc/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#:schema https://www.schemastore.org/pyproject.json

[project]
name = "example-plugins-grpc"

description = "Example of gRPC contract testing with the Pact protobuf plugin"

version = "1.0.0"
requires-python = ">=3.10"
dependencies = ["grpcio~=1.0", "protobuf~=7.34"]

[dependency-groups]
test = ["pact-python", "pytest~=9.0"]

[tool]
[tool.pytest.ini_options]
addopts = ["--import-mode=importlib"]

# The plugin examples import shared code via the `examples.plugins.*` package
# path (e.g. the generated protobuf stubs in `examples.plugins.proto`), so the
# repository root must be importable.
pythonpath = ["../../.."]

log_date_format = "%H:%M:%S"
log_format = "%(asctime)s.%(msecs)03d [%(levelname)-8s] %(name)s: %(message)s"
log_level = "NOTSET"

[tool.ruff]
extend = "../../../pyproject.toml"

[tool.uv.sources]
pact-python = { path = "../../../" }
Loading
Loading