How to Mock gRPC Services Without Proto Files in 2025
If you've worked with gRPC, you know the pain: proto files, code generation, version mismatches, and the endless cycle of recompilation every time you need to test a new endpoint. In 2025, there's a better way to mock gRPC services during development and testing—without writing or compiling a single .proto file.
The Proto File Problem
Traditional gRPC development follows this workflow:
- Write a
.protofile defining your service - Install protobuf compiler and gRPC plugins
- Generate client/server code in your target language
- Implement the service
- Repeat steps 1-4 for every change
When you need to mock a gRPC service for testing, you're forced through the same process. Want to test how your client handles a new field? Write the proto, recompile, regenerate. Testing error conditions? More proto changes.
This becomes especially painful in:
- Frontend development: Waiting for backend teams to finalize proto definitions
- Integration testing: Setting up complex proto compilation pipelines in CI/CD
- Exploratory testing: Rapidly iterating on API designs
- Third-party API mocking: When you don't control the proto definitions
The Solution: Dynamic gRPC with google.protobuf.Struct
The key insight is that gRPC services don't technically require strongly-typed messages. Google's protobuf.Struct type represents arbitrary JSON-like data, allowing you to create dynamic gRPC services that accept and return any structure.
Here's how it works:
google.protobuf.Struct Explained
google.protobuf.Struct is a well-known protobuf type that represents a JSON object. Instead of defining explicit message types like:
message User {
int32 id = 1;
string username = 2;
string email = 3;
}
You can use Struct to accept any JSON-like data:
import "google/protobuf/struct.proto";
service UserService {
rpc GetUser(google.protobuf.Struct) returns (google.protobuf.Struct);
}
This single service definition can handle any input and return any output—perfect for dynamic mocking.
gRPC Server Reflection
The second piece of the puzzle is gRPC server reflection. Reflection allows clients like grpcurl to discover available services and methods without needing proto files. When combined with dynamic message types, you get a completely proto-free workflow.
Step-by-Step: Mock gRPC Without Proto Files
Let's walk through creating and calling a mock gRPC service using rpcmock.com, which implements this dynamic approach.
Step 1: Create a Workspace and Mock
First, create a workspace and define your mock response:
# Create a workspace
curl -X POST https://rpcmock.com/api/v1/workspaces \
-H "Content-Type: application/json" \
-d '{"name": "My Test Workspace", "ttl_hours": 24}'
# Response:
# {
# "workspace_id": "<workspace_id>",
# "grpc_endpoint": "grpc.rpcmock.com:443",
# ...
# }
# Create a gRPC mock
curl -X POST https://rpcmock.com/api/v1/workspaces/<workspace_id>/mocks \
-H "Content-Type: application/json" \
-d '{
"protocol": "grpc",
"method": "/users.UserService/GetUser",
"response": {
"id": 42,
"username": "johndoe",
"email": "[email protected]",
"created_at": "2025-01-15T10:30:00Z",
"profile": {
"bio": "Software engineer",
"location": "San Francisco"
}
}
}'
No proto files needed. Just define your method name and response structure as JSON.
Step 2: Use gRPC Reflection to Discover Services
Now use grpcurl with reflection to discover available services:
# List all services (no -proto flag needed!)
grpcurl \
-H "workspace-id: <workspace_id>" \
grpc.rpcmock.com:443 \
list
# Output:
# grpc.reflection.v1alpha.ServerReflection
# users.UserService
The mock service appears automatically because the server dynamically generates proto descriptors from your mocks.
Step 3: Describe and Call the Service
Describe the service to see available methods:
# Describe the service
grpcurl \
-H "workspace-id: <workspace_id>" \
grpc.rpcmock.com:443 \
describe users.UserService
# Output shows the service uses google.protobuf.Struct types
Call the method with any JSON data:
# Call the method
grpcurl \
-H "workspace-id: <workspace_id>" \
-d '{"user_id": 42}' \
grpc.rpcmock.com:443 \
users.UserService/GetUser
# Response:
# {
# "id": 42,
# "username": "johndoe",
# "email": "[email protected]",
# "created_at": "2025-01-15T10:30:00Z",
# "profile": {
# "bio": "Software engineer",
# "location": "San Francisco"
# }
# }
Code Examples: Calling Dynamic gRPC Mocks
Go Client
package main
import (
"context"
"fmt"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/structpb"
)
func main() {
// Connect to mock server
conn, err := grpc.Dial("grpc.rpcmock.com:443",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Create request with google.protobuf.Struct
request, _ := structpb.NewStruct(map[string]interface{}{
"user_id": 42,
})
// Add workspace ID to metadata
ctx := metadata.AppendToOutgoingContext(
context.Background(),
"workspace-id", "<workspace_id>",
)
// Call the service using dynamic invocation
var response structpb.Struct
err = conn.Invoke(
ctx,
"/users.UserService/GetUser",
request,
&response,
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %+v\n", response.AsMap())
}
Python Client
import grpc
from google.protobuf import struct_pb2
# Connect to mock server
channel = grpc.insecure_channel('grpc.rpcmock.com:443')
# Create request
request = struct_pb2.Struct()
request.update({"user_id": 42})
# Add workspace ID to metadata
metadata = [('workspace-id', '<workspace_id>')]
# Call the service
response = channel.unary_unary(
'/users.UserService/GetUser',
request_serializer=lambda x: x.SerializeToString(),
response_deserializer=struct_pb2.Struct.FromString,
)(request, metadata=metadata)
print(f"Response: {dict(response)}")
Node.js Client
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
// Load google.protobuf.Struct
const structProto = grpc.loadPackageDefinition(
protoLoader.loadSync('google/protobuf/struct.proto')
);
// Create client
const client = new grpc.Client(
'grpc.rpcmock.com:443',
grpc.credentials.createInsecure()
);
// Create request
const request = {
fields: {
user_id: { numberValue: 42 }
}
};
// Add workspace ID to metadata
const metadata = new grpc.Metadata();
metadata.add('workspace-id', '<workspace_id>');
// Call the service
client.makeUnaryRequest(
'/users.UserService/GetUser',
(arg) => Buffer.from(JSON.stringify(arg)),
(arg) => JSON.parse(arg),
request,
metadata,
(error, response) => {
if (error) {
console.error(error);
} else {
console.log('Response:', response);
}
}
);
Advanced Mock Scenarios
Conditional Response Matching
Return different responses based on request parameters:
# Mock for admin users
curl -X POST https://rpcmock.com/api/v1/workspaces/<workspace_id>/mocks \
-H "Content-Type: application/json" \
-d '{
"protocol": "grpc",
"method": "/users.UserService/GetUser",
"match_conditions": {"role": "admin"},
"response": {
"id": 1,
"username": "admin",
"permissions": ["read", "write", "delete"]
},
"priority": 10
}'
# Default mock for regular users
curl -X POST https://rpcmock.com/api/v1/workspaces/<workspace_id>/mocks \
-H "Content-Type: application/json" \
-d '{
"protocol": "grpc",
"method": "/users.UserService/GetUser",
"response": {
"id": 42,
"username": "user",
"permissions": ["read"]
},
"priority": 1
}'
Error Simulation
Test error handling by returning gRPC error codes:
curl -X POST https://rpcmock.com/api/v1/workspaces/<workspace_id>/mocks \
-H "Content-Type: application/json" \
-d '{
"protocol": "grpc",
"method": "/users.UserService/GetUser",
"match_conditions": {"user_id": 999},
"error": {
"code": 5,
"message": "User not found"
}
}'
When you request user ID 999, the service returns a NOT_FOUND error (code 5).
Latency Simulation
Simulate network delays and timeouts:
curl -X POST https://rpcmock.com/api/v1/workspaces/<workspace_id>/mocks \
-H "Content-Type: application/json" \
-d '{
"protocol": "grpc",
"method": "/users.UserService/GetUser",
"response": {"id": 42, "username": "slowuser"},
"delay_ms": 5000
}'
This mock will wait 5 seconds before responding, perfect for testing timeout handling.
Traditional vs Dynamic Mocking Comparison
| Aspect | Traditional Mocking | Dynamic Mocking (RPC Mock) |
|---|---|---|
| Setup Time | Hours (proto files, compilation, server setup) | Seconds (single API call) |
| Proto Files | Required, must be maintained | Not needed |
| Code Generation | Required for each language | Not needed |
| Schema Changes | Recompile and redeploy | Instant via API |
| CI/CD Integration | Complex (build tools, dependencies) | Simple (HTTP API calls) |
| Learning Curve | Steep (protobuf, gRPC tooling) | Gentle (JSON + HTTP) |
| Reflection Support | Manual setup | Built-in |
| Version Management | Manual proto versioning | Ephemeral workspaces |
Best Practices for Dynamic gRPC Mocking
1. Use Descriptive Method Names
Follow gRPC naming conventions even without proto files:
/package.Service/Method
Examples:
/users.UserService/GetUser/payments.PaymentService/ProcessPayment/auth.v2.AuthService/RefreshToken
2. Leverage Conditional Matching
Create multiple mocks with different conditions to test edge cases:
// Success case
{ "user_id": 1 } → {"status": "active"}
// Error case
{ "user_id": -1 } → Error: Invalid ID
// Rate limit case
{ "requests": 100 } → Error: Rate limit exceeded
3. Simulate Production Behavior
Add realistic delays and occasional errors:
# success with 100ms latency
{
"response": {...},
"delay_ms": 100
}
# errors to test retry logic
{
"error": {"code": 14, "message": "Service unavailable"},
"match_conditions": {"test_error": true}
}
4. Use Isolated Workspaces
Create separate workspaces for different test scenarios:
ws_integration_tests: Stable mocks for CI/CDws_frontend_dev: Actively changing mocks for UI workws_load_testing: High-throughput mocks with latency
5. Monitor Mock Usage
Check request logs to verify your client is calling mocks correctly:
curl https://rpcmock.com/api/v1/workspaces/<workspace_id>/requests
This shows all requests, responses, and timing—invaluable for debugging.
When to Use Dynamic gRPC Mocking
Dynamic mocking is ideal for:
- ✅ Rapid prototyping: Test API designs without committing to proto schemas
- ✅ Frontend development: Build UIs before backend services are ready
- ✅ Integration testing: Mock external gRPC services without complex setup
- ✅ CI/CD pipelines: Fast, ephemeral mocks that require no build step
- ✅ API exploration: Experiment with different response structures
- ✅ Demo environments: Quickly spin up mock backends for presentations
Traditional proto-based mocking is better when:
- ❌ You need strict type checking and code generation
- ❌ Your team already has proto definitions and tooling
- ❌ You're testing proto-specific features (streaming, oneof, etc.)
Getting Started with RPC Mock
RPC Mock provides production-ready dynamic gRPC mocking with zero setup:
- No installation: Cloud-hosted, accessed via HTTPS/gRPC
- No authentication: Start mocking immediately
- Isolated workspaces: Each workspace gets unique endpoints
- Request logging: Built-in observability for debugging
- Conditional matching: Priority-based routing for complex scenarios
- Always free: Ephemeral workspaces with 24-hour expiration
Try it now:
# Create a workspace
WORKSPACE=$(curl -X POST https://rpcmock.com/api/v1/workspaces \
-H "Content-Type: application/json" \
-d '{"name": "Quick Test"}' | jq -r '.workspace_id')
# Create a mock
curl -X POST https://rpcmock.com/api/v1/workspaces/$WORKSPACE/mocks \
-H "Content-Type: application/json" \
-d '{
"protocol": "grpc",
"method": "/demo.Demo/Hello",
"response": {"message": "Hello from RPC Mock!"}
}'
# Call it with grpcurl
grpcurl \
-H "workspace-id: $WORKSPACE" \
-d '{"name": "World"}' \
grpc.rpcmock.com:443 \
demo.Demo/Hello
Conclusion
In 2025, you don't need to wrestle with proto files and code generation to mock gRPC services. By leveraging google.protobuf.Struct and gRPC reflection, you can create dynamic mocks that accept any input and return any output—all without writing a single .proto file.
This approach dramatically accelerates development workflows:
- Frontend teams can build against mock backends immediately
- Integration tests spin up in seconds without complex build steps
- API designs can be explored and iterated rapidly
- Teams spend less time managing tooling and more time building features
Whether you're building microservices, testing third-party integrations, or just exploring gRPC, dynamic mocking provides the flexibility and speed modern development demands.
Ready to try it? Head to rpcmock.com and create your first proto-free gRPC mock in under 30 seconds.