pytest-pubsub
In-memory mock of Google Cloud Pub/Sub topics and subscriptions, for use in tests. No network access, no credentials, no emulator process required. Compatible with Python 3.9+.
Install
pip install pytest-pubsub
How it works
Everything is backed by a single in-memory PubSubBroker that tracks topics, subscriptions,
and their pending/in-flight messages. Publishing to a topic fans the message out to every
subscription attached to it, matching real Pub/Sub delivery semantics. Nothing touches the
network — the broker is just plain Python data structures.
Errors mirror google.api_core.exceptions: operating on a topic or subscription that doesn't
exist raises NotFound; creating one that already exists raises AlreadyExists. Code written
against the real client's exception handling works unchanged against the mock.
Publisher usage
MockPublisherClient implements the subset of google.cloud.pubsub_v1.PublisherClient used in
practice — topic_path, create_topic/get_topic/delete_topic/list_topics, and publish.
It's constructed with an explicit PubSubBroker and passed into your code via dependency
injection — nothing is monkeypatched globally:
from pytest_pubsub.publisher import MockPublisherClient
from pytest_pubsub.registry import PubSubBroker
broker = PubSubBroker()
client = MockPublisherClient(broker)
topic_path = client.topic_path("my-project", "events")
client.create_topic(name=topic_path)
future = client.publish(topic_path, b"hello", source="test")
message_id = future.result()
Subscriber usage
MockSubscriberClient implements the subset of google.cloud.pubsub_v1.SubscriberClient used
in practice — subscription_path, create_subscription/get_subscription/
delete_subscription, synchronous pull, acknowledge, modify_ack_deadline, and a simplified
subscribe() for streaming-pull-style consumers:
from pytest_pubsub.registry import PubSubBroker
from pytest_pubsub.publisher import MockPublisherClient
from pytest_pubsub.subscriber import MockSubscriberClient
broker = PubSubBroker()
publisher = MockPublisherClient(broker)
subscriber = MockSubscriberClient(broker)
topic_path = publisher.topic_path("my-project", "events")
publisher.create_topic(name=topic_path)
sub_path = subscriber.subscription_path("my-project", "events-sub")
subscriber.create_subscription(name=sub_path, topic=topic_path)
publisher.publish(topic_path, b"hello")
response = subscriber.pull(subscription=sub_path, max_messages=1)
message = response.received_messages[0].message
assert message.data == b"hello"
message.ack()
Or with a streaming-pull-style callback:
def handle(message):
print(message.data)
message.ack()
future = subscriber.subscribe(sub_path, handle)
# ... later
future.cancel()
future.result(timeout=1)
pytest fixtures
Installing the package registers a pytest plugin (via the pytest11 entry point), so these
fixtures are available in any test suite with no extra configuration:
| Fixture | Description |
|---|---|
pubsub_broker |
Fresh, isolated PubSubBroker for the test |
mock_publisher_client |
MockPublisherClient bound to pubsub_broker |
mock_subscriber_client |
MockSubscriberClient bound to the same pubsub_broker |
pubsub_topic_factory |
factory(project, topic) -> MockTopic, creates the topic |
pubsub_subscription_factory |
factory(project, subscription, topic_path) -> MockSubscription |
No monkeypatching happens automatically. Your code under test should accept a client instance (constructor injection, a factory function, etc.) so the fixture can be passed in explicitly:
# app/publisher.py — code under test, accepts an injected client
def send_event(client, topic_path: str, payload: bytes):
future = client.publish(topic_path, payload)
return future.result()
# tests/test_publisher.py
def test_send_event(mock_publisher_client, mock_subscriber_client, pubsub_broker):
topic_path = mock_publisher_client.topic_path("proj", "events")
mock_publisher_client.create_topic(name=topic_path)
sub_path = mock_subscriber_client.subscription_path("proj", "events-sub")
mock_subscriber_client.create_subscription(name=sub_path, topic=topic_path)
from app.publisher import send_event
message_id = send_event(mock_publisher_client, topic_path, b"hello")
assert message_id
response = mock_subscriber_client.pull(subscription=sub_path, max_messages=1)
assert response.received_messages[0].message.data == b"hello"
mock_subscriber_client.acknowledge(
subscription=sub_path,
ack_ids=[response.received_messages[0].ack_id],
)
Limitations / non-goals
- Client-level mock only. There is no fake gRPC/HTTP server — this does not emulate the
Pub/Sub emulator wire protocol, so it's not a drop-in for tests that talk to the emulator over
the network. Code under test must accept a client instance instead of constructing
google.cloud.pubsub_v1.PublisherClient/SubscriberClientdirectly. - No automatic ack-deadline expiry.
pull/acknowledgeare synchronous and in-process, so messages never redeliver on their own after a deadline; usemessage.nack()ormodify_ack_deadline(..., ack_deadline_seconds=0)to force redelivery in a test. - No IAM, retention policies, dead-lettering, or push subscriptions.
Running the tests
make install # uv sync --all-extras
make test # uv run pytest --cov=pytest_pubsub
Other Makefile targets:
make lint # ruff check
make format # ruff format
make format-check # ruff format --check
make typecheck # mypy --strict
make check # lint + format-check + typecheck + test
Linting and type checking
make install
uv run pre-commit install # runs ruff and mypy automatically on every commit
uv run pre-commit run --all-files
CI (.github/workflows/ci.yml) runs make check (ruff check, ruff format --check, mypy --strict, and the test suite) across Python 3.9–3.14 on every pull request and on every push to
main.
Contributing
Issues and pull requests are welcome. Please include tests for any behavior change — every source change in this repo ships with its own tests in the same commit.
License
MIT
Release files for pytest-pubsub 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytest_pubsub-0.1.1.tar.gz | 119.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_pubsub-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 132.0 kB
Release files / pytest_pubsub-0.1.1.tar.gz
| Download URL | pytest_pubsub-0.1.1.tar.gz |
|---|---|
| Size | 119.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
00d577bb86a756a0ca46eb9e5951209ad387f4e4b623fbf28f1951152d6b5842
|
|
BLAKE2b-256 checksum How to use checksums |
92947137be6f9132317d5684f23a2ee2b34d4c7ead8edc10d8f45668aa3b7ac4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.
Transparency logRelease files / pytest_pubsub-0.1.1-py3-none-any.whl
| Download URL | pytest_pubsub-0.1.1-py3-none-any.whl |
|---|---|
| Size | 12.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e8b51a7a64cc0acd9bb13b53df8dd7d84170d30e5d6e25749ca04bb2b25ed5fd
|
|
BLAKE2b-256 checksum How to use checksums |
5563814f91a63a5f4725a9b6d85897c42575fe0641e132e753f3ac115e5c7e23
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.
Transparency log