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:

  1. Write a .proto file defining your service
  2. Install protobuf compiler and gRPC plugins
  3. Generate client/server code in your target language
  4. Implement the service
  5. 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

AspectTraditional MockingDynamic Mocking (RPC Mock)
Setup TimeHours (proto files, compilation, server setup)Seconds (single API call)
Proto FilesRequired, must be maintainedNot needed
Code GenerationRequired for each languageNot needed
Schema ChangesRecompile and redeployInstant via API
CI/CD IntegrationComplex (build tools, dependencies)Simple (HTTP API calls)
Learning CurveSteep (protobuf, gRPC tooling)Gentle (JSON + HTTP)
Reflection SupportManual setupBuilt-in
Version ManagementManual proto versioningEphemeral 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/CD
  • ws_frontend_dev: Actively changing mocks for UI work
  • ws_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.

How to Mock gRPC Services Without Proto Files in 2025 - RPC Mock