Skip to content

gRPC: Code Examples (Python + Pydantic + Playwright)

Playwright is HTTP-based, so for gRPC we use grpcio for native calls and Pydantic for validation. We also show how to test gRPC-Gateway (HTTP/JSON proxy) with Playwright's API context.


1. Proto-to-Pydantic Models

from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum


class UserStatus(str, Enum):
    """Maps to protobuf UserStatus enum."""

    active = "ACTIVE"
    inactive = "INACTIVE"


class UserProto(BaseModel):
    """Pydantic model matching protobuf User message."""

    id: str
    name: str = Field(min_length=1, max_length=100)
    email: str
    age: Optional[int] = Field(default=None, ge=0)
    status: UserStatus = UserStatus.active


class ListUsersResponse(BaseModel):
    """Matches protobuf ListUsersResponse message."""

    users: list[UserProto]
    next_page_token: str = ""
    total_count: int = Field(ge=0)


class GrpcError(BaseModel):
    """Structured gRPC error for validation."""

    code: int
    message: str
    details: list[dict] = Field(default_factory=list)

2. gRPC Native Client Tests (grpcio)

import grpc
import pytest
from user_pb2 import GetUserRequest, ListUsersRequest, CreateUserRequest
from user_pb2_grpc import UserServiceStub

GRPC_TARGET = "localhost:50051"
TIMEOUT_SEC = 5


@pytest.fixture()
def channel():
    """Create gRPC channel, close after test."""
    ch = grpc.insecure_channel(GRPC_TARGET)
    yield ch
    ch.close()


@pytest.fixture()
def stub(channel):
    """Create UserService stub."""
    return UserServiceStub(channel)


def test_unary_get_user(stub):
    """Test unary RPC — get single user."""
    request = GetUserRequest(id="user-123")
    response = stub.GetUser(request, timeout=TIMEOUT_SEC)
    user = UserProto.model_validate(
        {"id": response.id, "name": response.name, "email": response.email}
    )
    assert user.id == "user-123"
    assert user.name != ""


def test_create_user(stub):
    """Test unary RPC — create user."""
    request = CreateUserRequest(name="Alice", email="alice@example.com", age=30)
    response = stub.CreateUser(request, timeout=TIMEOUT_SEC)
    assert response.id != ""
    assert response.name == "Alice"


def test_server_streaming_list_users(stub):
    """Test server streaming — receive multiple users."""
    request = ListUsersRequest(page_size=10)
    users = []
    for user_msg in stub.ListUsers(request, timeout=TIMEOUT_SEC * 2):
        user = UserProto.model_validate(
            {"id": user_msg.id, "name": user_msg.name, "email": user_msg.email}
        )
        users.append(user)
    assert len(users) <= 10


def test_not_found_error(stub):
    """Test error handling — missing resource returns NOT_FOUND."""
    with pytest.raises(grpc.RpcError) as exc_info:
        stub.GetUser(GetUserRequest(id="nonexistent"), timeout=TIMEOUT_SEC)
    assert exc_info.value.code() == grpc.StatusCode.NOT_FOUND


def test_invalid_argument(stub):
    """Test error handling — empty ID returns INVALID_ARGUMENT."""
    with pytest.raises(grpc.RpcError) as exc_info:
        stub.GetUser(GetUserRequest(id=""), timeout=TIMEOUT_SEC)
    assert exc_info.value.code() == grpc.StatusCode.INVALID_ARGUMENT


def test_deadline_exceeded(stub):
    """Test timeout — very short deadline triggers DEADLINE_EXCEEDED."""
    with pytest.raises(grpc.RpcError) as exc_info:
        stub.GetUser(GetUserRequest(id="user-123"), timeout=0.0001)
    assert exc_info.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED

3. gRPC-Gateway Tests with Playwright (HTTP/JSON proxy)

import pytest
from playwright.sync_api import Playwright

BASE_URL = "http://localhost:8080"


@pytest.fixture(scope="session")
def api_context(playwright: Playwright):
    """Create Playwright API context for gRPC-Gateway."""
    ctx = playwright.request.new_context(
        base_url=BASE_URL,
        extra_http_headers={"Content-Type": "application/json"},
    )
    yield ctx
    ctx.dispose()


def test_grpc_gateway_get_user(api_context):
    """Test gRPC-Gateway — GET user by ID."""
    response = api_context.get("/v1/users/user-123")
    assert response.status == 200
    user = UserProto.model_validate(response.json())
    assert user.id == "user-123"


def test_grpc_gateway_create_user(api_context):
    """Test gRPC-Gateway — POST create user."""
    payload = {"name": "Bob", "email": "bob@example.com", "age": 25}
    response = api_context.post("/v1/users", data=payload)
    assert response.status == 200
    user = UserProto.model_validate(response.json())
    assert user.name == "Bob"


def test_grpc_gateway_list_users(api_context):
    """Test gRPC-Gateway — GET list users with pagination."""
    response = api_context.get("/v1/users?page_size=5")
    assert response.status == 200
    data = ListUsersResponse.model_validate(response.json())
    assert len(data.users) <= 5
    assert data.total_count >= 0


def test_grpc_gateway_not_found(api_context):
    """Test gRPC-Gateway — 404 for missing resource."""
    response = api_context.get("/v1/users/nonexistent")
    assert response.status == 404
    error = GrpcError.model_validate(response.json())
    assert error.code == 5  # NOT_FOUND


def test_grpc_gateway_invalid_request(api_context):
    """Test gRPC-Gateway — 400 for bad request."""
    payload = {"name": "", "email": "invalid"}
    response = api_context.post("/v1/users", data=payload)
    assert response.status == 400