gRPC Fundamentals
gRPC uses Protocol Buffers (protobuf) for schema definition and HTTP/2 for transport. It is common in microservice architectures where services need high-performance, type-safe communication. As a QA engineer, you may not test gRPC as frequently as REST, but understanding it is increasingly important.
How gRPC Differs from REST
| Aspect | REST | gRPC |
|---|---|---|
| Protocol | HTTP/1.1 or HTTP/2 | HTTP/2 only |
| Data format | JSON (text) | Protocol Buffers (binary) |
| Schema | Optional (OpenAPI) | Required (.proto files) |
| Code generation | Optional | Built-in (generates client/server code) |
| Streaming | Not native | Native (server, client, bidirectional) |
| Browser support | Native | Requires gRPC-Web proxy |
| Error codes | HTTP status codes | gRPC status codes (own system) |
Protocol Buffers
gRPC services are defined in .proto files:
syntax = "proto3";
package user;
service UserService {
rpc GetUser(GetUserRequest) returns (UserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (UserResponse);
rpc UpdateUser(UpdateUserRequest) returns (UserResponse);
rpc DeleteUser(DeleteUserRequest) returns (Empty);
}
message GetUserRequest {
string id = 1;
}
message UserResponse {
string id = 1;
string name = 2;
string email = 3;
string role = 4;
string created_at = 5;
}
message ListUsersRequest {
int32 page = 1;
int32 per_page = 2;
}
message ListUsersResponse {
repeated UserResponse users = 1;
int32 total = 2;
}
The .proto file is the contract. Both client and server generate code from it, ensuring type safety.
Exploring with grpcurl
grpcurl is like curl for gRPC — essential for manual exploration and debugging.
# List available services
grpcurl -plaintext localhost:50051 list
# List methods in a service
grpcurl -plaintext localhost:50051 list user.UserService
# Describe a method
grpcurl -plaintext localhost:50051 describe user.UserService.GetUser
# Call a method
grpcurl -plaintext -d '{"id": "123"}' localhost:50051 user.UserService/GetUser
# Call with headers (metadata)
grpcurl -plaintext \
-H "Authorization: Bearer token123" \
-d '{"id": "123"}' \
localhost:50051 user.UserService/GetUser
# Call without TLS (-plaintext) for local development
# Call with TLS for staging/production (omit -plaintext)
grpcurl -d '{"id": "123"}' api.staging.example.com:443 user.UserService/GetUser
Testing gRPC with Python
import grpc
import user_pb2
import user_pb2_grpc
@pytest.fixture(scope="session")
def grpc_channel():
channel = grpc.insecure_channel("localhost:50051")
yield channel
channel.close()
@pytest.fixture
def user_service(grpc_channel):
return user_pb2_grpc.UserServiceStub(grpc_channel)
def test_get_user(user_service):
request = user_pb2.GetUserRequest(id="123")
response = user_service.GetUser(request)
assert response.name == "Alice"
assert response.email == "alice@test.com"
def test_get_nonexistent_user(user_service):
request = user_pb2.GetUserRequest(id="nonexistent")
with pytest.raises(grpc.RpcError) as exc_info:
user_service.GetUser(request)
assert exc_info.value.code() == grpc.StatusCode.NOT_FOUND
def test_create_user(user_service):
request = user_pb2.CreateUserRequest(
name="New User",
email="new@test.com",
role="viewer"
)
response = user_service.CreateUser(request)
assert response.id != ""
assert response.name == "New User"
gRPC Status Codes
gRPC uses its own status code system, not HTTP codes:
| gRPC Code | Meaning | REST Equivalent |
|---|---|---|
OK |
Success | 200 |
INVALID_ARGUMENT |
Client sent bad data | 400 |
NOT_FOUND |
Resource does not exist | 404 |
ALREADY_EXISTS |
Duplicate creation | 409 |
PERMISSION_DENIED |
Insufficient permissions | 403 |
UNAUTHENTICATED |
Missing or invalid auth | 401 |
RESOURCE_EXHAUSTED |
Rate limit or quota | 429 |
INTERNAL |
Server error | 500 |
UNAVAILABLE |
Service is down | 503 |
DEADLINE_EXCEEDED |
Request timed out | 504 |
UNIMPLEMENTED |
Method not implemented | 501 |
Streaming
gRPC supports four communication patterns:
| Pattern | Description | Use Case |
|---|---|---|
| Unary | Client sends one request, server sends one response | Standard CRUD |
| Server streaming | Client sends one request, server sends stream of responses | Live feed, log tailing |
| Client streaming | Client sends stream of requests, server sends one response | File upload, batch processing |
| Bidirectional | Both sides send streams simultaneously | Chat, real-time collaboration |
Testing Server Streaming
def test_server_streaming(user_service):
"""Server streams all users matching a query."""
request = user_pb2.ListUsersRequest(page=1, per_page=100)
users = []
for response in user_service.StreamUsers(request):
users.append(response)
assert len(users) > 0
assert all(hasattr(u, "name") for u in users)
Deadline/Timeout Testing
gRPC has built-in deadline propagation — a client can set a deadline, and if the server does not respond in time, the call fails.
def test_deadline_exceeded(user_service):
"""Request with very short deadline should fail."""
request = user_pb2.GetUserRequest(id="123")
with pytest.raises(grpc.RpcError) as exc_info:
# 1 microsecond deadline — guaranteed to fail
user_service.GetUser(request, timeout=0.000001)
assert exc_info.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED
Practical Exercise
- Install grpcurl and explore a gRPC service (use a public demo or set up a local one)
- List available services and methods
- Make a unary call and inspect the response
- Write Python tests for a gRPC service: success case, not found, invalid argument
- Test deadline handling with a very short timeout
Key Takeaways
- gRPC uses Protocol Buffers (binary, typed) instead of JSON (text, untyped)
- grpcurl is the essential tool for manual gRPC exploration
- gRPC has its own status code system — learn the mappings to HTTP codes
- Test streaming (server, client, bidirectional) when the service supports it
- Deadline propagation is built-in — test timeout behavior
- The
.protofile is the source of truth for the API contract