Advanced Patterns¶
Advanced usage patterns and techniques for grpcvcr.
Custom Matchers¶
Method-Only Matching¶
from grpcvcr import MethodMatcher, recorded_channel
target = "localhost:50051"
with recorded_channel(
"test.yaml",
target,
match_on=MethodMatcher(),
) as channel:
stub = MyServiceStub(channel)
# Any GetUser request will match the first recorded GetUser response
response = stub.GetUser(GetUserRequest(id=1))
Metadata Matching¶
from grpcvcr import MetadataMatcher, recorded_channel
target = "localhost:50051"
with recorded_channel(
"test.yaml",
target,
match_on=MetadataMatcher(keys=["authorization"]),
) as channel:
stub = MyServiceStub(channel)
response = stub.GetUser(
GetUserRequest(id=1),
metadata=[("authorization", "***")],
)
Combining Matchers¶
from grpcvcr import MetadataMatcher, MethodMatcher, RequestMatcher
# Match method AND request body
matcher = MethodMatcher() & RequestMatcher()
# Match method AND specific metadata keys
matcher = MethodMatcher() & MetadataMatcher(keys=["authorization"])
Custom Match Functions¶
from grpcvcr import CustomMatcher, MethodMatcher
from grpcvcr.serialization import InteractionRequest
def match_by_user_id(request: InteractionRequest, recorded: InteractionRequest) -> bool:
"""Match if the user ID in the request body matches."""
# Parse your protobuf and compare specific fields
req_body = YourRequest.FromString(request.get_body_bytes())
rec_body = YourRequest.FromString(recorded.get_body_bytes())
return req_body.user_id == rec_body.user_id
matcher = MethodMatcher() & CustomMatcher(func=match_by_user_id)
Cassette Formats¶
YAML (Default)¶
# test.yaml
interactions:
- request:
method: /myservice.MyService/GetUser
body_base64: CAE=
metadata: []
response:
body_base64: CgVBbGljZQ==
code: OK
details: ""
JSON¶
from grpcvcr import recorded_channel
target = "localhost:50051"
with recorded_channel("test.json", target) as channel:
...
The format is determined by the file extension.
Error Handling¶
Catching Recording Errors¶
from grpcvcr import RecordMode, recorded_channel
from grpcvcr.errors import RecordingDisabledError
target = "localhost:50051"
try:
with recorded_channel(
"test.yaml", target, record_mode=RecordMode.NONE
) as channel:
stub = MyServiceStub(channel)
# This will fail if not in cassette
stub.GetUser(GetUserRequest(id=999))
except RecordingDisabledError as e:
print(f"No recorded interaction for: {e.method}")
Secure Channels¶
import grpc
from grpcvcr import recorded_channel
credentials = grpc.ssl_channel_credentials()
with recorded_channel(
"test.yaml",
"api.example.com:443",
credentials=credentials,
) as channel:
stub = MyServiceStub(channel)
response = stub.GetUser(GetUserRequest(id=1))
Channel Options¶
from grpcvcr import recorded_channel
target = "localhost:50051"
with recorded_channel(
"test.yaml",
target,
options=[
("grpc.max_receive_message_length", 1024 * 1024 * 10),
("grpc.max_send_message_length", 1024 * 1024 * 10),
],
) as channel:
...
Recording gRPC Errors¶
gRPC errors are recorded and replayed:
import grpc
from grpcvcr import RecordMode, recorded_channel
target = "localhost:50051"
# Record an error response
with recorded_channel("error_test.yaml", target) as channel:
stub = MyServiceStub(channel)
try:
stub.GetUser(GetUserRequest(id=999)) # Returns NOT_FOUND
except grpc.RpcError as e:
assert e.code() == grpc.StatusCode.NOT_FOUND
# Replay the error
with recorded_channel(
"error_test.yaml", target, record_mode=RecordMode.NONE
) as channel:
stub = MyServiceStub(channel)
try:
stub.GetUser(GetUserRequest(id=999))
except grpc.RpcError as e:
# Same error is replayed
assert e.code() == grpc.StatusCode.NOT_FOUND
Parallel Test Isolation¶
import pytest
from grpcvcr import Cassette, RecordingChannel
@pytest.fixture
def cassette_path(tmp_path, request):
"""Generate unique cassette path per test."""
return tmp_path / f"{request.node.name}.yaml"
def test_one(cassette_path, grpc_target):
cassette = Cassette(cassette_path)
...
def test_two(cassette_path, grpc_target):
cassette = Cassette(cassette_path)
...