pytest-grpc-aio
Write tests for gRPC services with pytest using grpc.aio (asyncio-based gRPC).
Installation
pip install pytest-grpc-aio
Features
- Async/await support - Built on
grpc.aiofor native asyncio integration - Flexible servicer configuration - Provide servicers via fixtures or at test time
- Real and fake servers - Run tests against actual gRPC servers or direct Python calls
- Context tracking - Access server/channel details via
grpc_contextfixture - Type-safe - Full type hints with Protocol-based interfaces
Quick Start
1. Define Your Service
Given a proto file:
syntax = "proto3";
package example.v1;
service EchoService {
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
Implement your servicer:
from example_pb2 import HelloRequest, HelloResponse
from example_pb2_grpc import EchoServiceServicer
class EchoServicer(EchoServiceServicer):
async def SayHello(self, request: HelloRequest, context) -> HelloResponse:
return HelloResponse(message=f"Hello, {request.name}!")
2. Configure Test Fixtures
import pytest
from example_pb2_grpc import add_EchoServiceServicer_to_server, EchoServiceStub
from servicer import EchoServicer
@pytest.fixture
def grpc_add_to_server():
"""Required: Tell pytest how to register your servicer."""
return add_EchoServiceServicer_to_server
@pytest.fixture
def grpc_stub_cls():
"""Required: Tell pytest which stub class to use."""
return EchoServiceStub
# Option 1: Provide servicer via fixture (module/session scope)
@pytest.fixture(scope="module")
def grpc_servicer():
"""Optional: Provide a default servicer for all tests."""
return EchoServicer()
3. Write Tests
Using the Fixture-Provided Servicer
import pytest
from example_pb2 import HelloRequest
@pytest.mark.asyncio
async def test_say_hello(grpc_aio_stub):
"""Test using the servicer from grpc_servicer fixture."""
async with grpc_aio_stub() as stub:
request = HelloRequest(name="World")
response = await stub.SayHello(request)
assert response.message == "Hello, World!"
Providing Servicer Per-Test
@pytest.mark.asyncio
async def test_with_custom_servicer(grpc_aio_stub):
"""Override the servicer for a specific test."""
custom_servicer = EchoServicer()
async with grpc_aio_stub(servicer=custom_servicer) as stub:
request = HelloRequest(name="Custom")
response = await stub.SayHello(request)
assert response.message == "Hello, Custom!"
Testing Error Handling
import grpc
class ErrorServicer(EchoServiceServicer):
async def SayHello(self, request: HelloRequest, context):
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid name")
@pytest.mark.asyncio
async def test_error_handling(grpc_aio_stub):
"""Test error scenarios."""
error_servicer = ErrorServicer()
async with grpc_aio_stub(servicer=error_servicer) as stub:
request = HelloRequest(name="")
with pytest.raises(grpc.RpcError) as exc_info:
await stub.SayHello(request)
assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT
Advanced Usage
Working with Credentials
import grpc
from pathlib import Path
@pytest.fixture
def my_channel_credentials():
"""Provide SSL credentials for secure channels."""
cert_path = Path("/path/to/cert.pem")
return grpc.ssl_channel_credentials(
root_certificates=cert_path.read_bytes()
)
@pytest.mark.asyncio
async def test_secure_connection(grpc_aio_stub, my_channel_credentials):
"""Test with SSL/TLS credentials."""
async with grpc_aio_stub(credentials=my_channel_credentials) as stub:
request = HelloRequest(name="Secure")
response = await stub.SayHello(request)
assert response.message == "Hello, Secure!"
Using Channel Options
@pytest.mark.asyncio
async def test_with_options(grpc_aio_stub):
"""Test with custom channel options."""
options = [
("grpc.max_receive_message_length", 1024 * 1024 * 10),
("grpc.max_send_message_length", 1024 * 1024 * 10),
]
async with grpc_aio_stub(options=options) as stub:
request = HelloRequest(name="Options")
response = await stub.SayHello(request)
assert response.message == "Hello, Options!"
Accessing gRPC Context
The grpc_context fixture provides access to:
grpc_context.addr: server addressgrpc_context.server: the gRPC server instancegrpc_context.channel: the gRPC channel instancegrpc_context.servicer: the servicer being testedgrpc_context.add_to_server: theadd_to_serverfunctiongrpc_context.interceptors: any configured interceptors
@pytest.mark.asyncio
async def test_inspect_context(grpc_aio_stub, grpc_context):
"""Access server and channel details via grpc_context."""
async with grpc_aio_stub() as stub:
print(f"Testing against server at {grpc_context.addr}")
request = HelloRequest(name="Context")
response = await stub.SayHello(request)
assert response.message == "Hello, Context!"
Using Interceptors
import grpc.aio
class LoggingInterceptor(grpc.aio.ServerInterceptor):
async def intercept_service(self, continuation, handler_call_details):
print(f"Handling RPC: {handler_call_details.method}")
return await continuation(handler_call_details)
@pytest.fixture
def grpc_interceptors():
"""Add server interceptors."""
return [LoggingInterceptor()]
Lower-Level Access
If you need more control, you can use the channel or server fixtures directly:
@pytest.mark.asyncio
async def test_with_channel(grpc_aio_channel, grpc_stub_cls):
"""Use the channel fixture directly."""
servicer = EchoServicer()
async with grpc_aio_channel(servicer) as channel:
stub = grpc_stub_cls(channel)
request = HelloRequest(name="Channel")
response = await stub.SayHello(request)
assert response.message == "Hello, Channel!"
@pytest.mark.asyncio
async def test_with_server(grpc_aio_server, grpc_addr, grpc_stub_cls):
"""Use the server fixture directly."""
servicer = EchoServicer()
async with grpc_aio_server(servicer):
async with grpc.aio.insecure_channel(grpc_addr) as channel:
stub = grpc_stub_cls(channel)
request = HelloRequest(name="Server")
response = await stub.SayHello(request)
assert response.message == "Hello, Server!"
Command-Line Options
The plugin provides several command-line options:
Fake Server Mode
Run tests by calling service handlers directly (no real gRPC server):
pytest --grpc-fake-server
Benefits:
- Faster test execution
- Direct exception propagation (easier debugging)
- No network overhead
Trade-offs:
- Doesn't test actual gRPC serialization/networking
- May miss integration issues
Example output with fake server:
def test_error_handling(grpc_aio_stub):
async with grpc_aio_stub(servicer=ErrorServicer()) as stub:
request = HelloRequest(name="")
> response = await stub.SayHello(request)
test_example.py:45:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
async def SayHello(self, request: HelloRequest, context):
> await context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid name")
E FakeRpcError: Invalid name
servicer.py:12: FakeRpcError
Configuring Worker Threads
Control the ThreadPoolExecutor used by gRPC servers:
# Set maximum workers via command line
pytest --grpc-max-workers=10
# Or in a test module
grpc_max_workers = 10
The effective value is the maximum of the CLI option and module variable.
Use cases:
- Test thread-safety of servicers
- Simulate concurrent client requests
- Stress test resource locking
Available Fixtures
Required User Fixtures
| Fixture | Scope | Description |
|---|---|---|
grpc_add_to_server |
Any | Function that registers your servicer with a gRPC server |
grpc_stub_cls |
Any | Your stub class (e.g., from *_pb2_grpc.py) |
Optional User Fixtures
| Fixture | Scope | Description |
|---|---|---|
grpc_servicer |
Any | Default servicer instance to use in tests |
grpc_interceptors |
Any | List of gRPC interceptors |
Provided Fixtures
| Fixture | Description |
|---|---|
grpc_aio_stub |
Most useful - Returns a factory for creating async stubs with context managers |
grpc_aio_channel |
Returns a factory for creating async channels with context managers |
grpc_aio_server |
Returns a factory for creating async servers with context managers |
grpc_context |
Provides access to server/channel details during tests |
grpc_addr |
The address where the test server is listening |
Synchronous gRPC (grpc.Server)
For legacy/sync gRPC code, use the non-aio fixtures:
def test_sync(grpc_stub):
"""Use synchronous gRPC (no async/await)."""
with grpc_stub() as stub:
request = HelloRequest(name="Sync")
response = stub.SayHello(request)
assert response.message == "Hello, Sync!"
Available sync fixtures: grpc_stub, grpc_channel, grpc_server
Type Safety
All fixtures and factories are fully typed using typing.Protocol:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pytest_grpc_aio import GrpcAioStubFactory, GrpcContext
# Your IDE will provide autocomplete and type checking
stub_factory: GrpcAioStubFactory[EchoServiceStub, EchoServicer]
context: GrpcContext
Migration from pytest-grpc
If you're migrating from the original pytest-grpc:
- Use async fixtures: Replace
grpc_stubwithgrpc_aio_stub - Use context managers: Stubs are now created via
async with grpc_aio_stub() as stub: - Pass servicers explicitly: Can override
grpc_servicerby passingservicer=argument - Add
@pytest.mark.asyncio: Required for async test functions
Examples
See the example/ directory for complete working examples including:
- Basic echo service tests
- Error handling
- Secure connections
- Custom interceptors
- Thread-safety testing
Contributing
Contributions welcome! Please open an issue or PR on GitHub.
License
MIT
Metadata
Release files for pytest-grpc-aio 0.3.0
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_grpc_aio-0.3.0.tar.gz | 11.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_grpc_aio-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 20.1 kB
Release files / pytest_grpc_aio-0.3.0.tar.gz
| Download URL | pytest_grpc_aio-0.3.0.tar.gz |
|---|---|
| Size | 11.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7615876169ae2bce67613f1147c69fcbe786eec12361a364318365f07e5b55a3
|
|
BLAKE2b-256 checksum How to use checksums |
d3faa0ca12cb44d18624a9ed520cdf2e1b22ee41e47c4c8528bcd6612ab228d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.6.16
|
Release files / pytest_grpc_aio-0.3.0-py3-none-any.whl
| Download URL | pytest_grpc_aio-0.3.0-py3-none-any.whl |
|---|---|
| Size | 8.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a143fdb577568d6d6b7aee6f33f2150546c79f7bedbf8705a0c73633adab47d1
|
|
BLAKE2b-256 checksum How to use checksums |
5d24401aa9ecfa8ff03034e7b816401a67fbe3fc32c9286f9a5cb75f6e79c1e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.6.16
|