Skip to content

gRPC Mocks and Fakes — Building Test Doubles for Reliable Service Testing

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about grpc mocks and fakes. We cover key concepts, practical examples, and best practices to help you master this topic.

gRPC mocking creates lightweight test doubles that simulate gRPC service behavior, enabling fast, deterministic unit tests without requiring real servers, databases, or network connections.

What You'll Learn

  • Generating mock clients from protobuf definitions
  • Using gomock for Go gRPC services
  • Using unittest.mock for Python gRPC clients
  • Building fake servers for integration tests
  • Testing error, timeout, and retry scenarios
  • Verifying call counts and arguments

Why It Matters

Real gRPC servers are slow to start, require network ports, and have dependencies on databases and other services. Mocks run in milliseconds, simulate any error condition, and verify exactly what the client calls. DodaTech's Durga Antivirus Pro uses generated mocks for all 50+ gRPC services, running 2000+ tests in under 10 seconds on every commit.

Real-World Use

A developer writes a client that retries on UNAVAILABLE errors. The mock server is configured to return UNAVAILABLE for the first two calls and succeed on the third. The test verifies the client retries exactly twice with the expected backoff, then successfully processes the response.

sequenceDiagram
    participant Test
    participant MockServer
    participant ClientUnderTest
    Test->>MockServer: Expect ReportThreat(req)
    MockServer-->>ClientUnderTest: UNAVAILABLE
    Test->>MockServer: Expect ReportThreat(req)
    MockServer-->>ClientUnderTest: UNAVAILABLE
    Test->>MockServer: Expect ReportThreat(req)
    MockServer-->>ClientUnderTest: Success
    Test->>ClientUnderTest: Assert 3 calls made

Code Examples

Example 1: Generated Mock with gomock (Go)

package main

//go:generate mockgen -source=threat/v1/threat_grpc.pb.go \
//    -destination=threat/v1/mock_threat.go \
//    -package=threatv1

import (
    "testing"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "google.golang.org/protobuf/proto"
    
    threatv1 "path/to/threat/v1"
)

func TestReportThreat_RetryOnUnavailable(t *testing.T) {
    // Create mock
    mockClient := threatv1.NewMockThreatServiceClient(gomock.NewController(t))
    
    // Set up expectations: first two calls fail, third succeeds
    firstCall := mockClient.EXPECT().
        ReportThreat(gomock.Any(), gomock.Any()).
        Return(nil, status.Error(codes.Unavailable, "server restarting"))
    
    secondCall := mockClient.EXPECT().
        ReportThreat(gomock.Any(), gomock.Any()).
        Return(nil, status.Error(codes.Unavailable, "server restarting"))
    
    thirdCall := mockClient.EXPECT().
        ReportThreat(gomock.Any(), gomock.Any()).
        Return(&threatv1.ThreatResponse{
            ThreatId: "threat-123",
            Status:   threatv1.ThreatStatus_QUARANTINED,
        }, nil)
    
    // Run client under test
    client := NewThreatClient(mockClient, RetryConfig{
        MaxRetries: 3,
        BaseDelay:  10 * time.Millisecond,
    })
    
    resp, err := client.ReportThreat(context.Background(),
        &threatv1.ThreatRequest{Name: "Malware-X"})
    
    if err != nil {
        t.Fatalf("Expected success, got %v", err)
    }
    if resp.ThreatId != "threat-123" {
        t.Errorf("Expected threat-123, got %s", resp.ThreatId)
    }
    
    // Verify all expectations met
    gomock.AssertExpectations(t)
}

Example 2: Python Mock for gRPC Client

import unittest
from unittest.mock import MagicMock, patch, call
import grpc

class TestThreatClient(unittest.TestCase):
    def setUp(self):
        # Create mock stub
        self.mock_stub = MagicMock()
        self.client = ThreatClient(self.mock_stub)
    
    def test_report_threat_success(self):
        # Configure mock response
        self.mock_stub.ReportThreat.return_value = pb.ThreatResponse(
            threat_id="threat-123",
            status=pb.ThreatStatus.QUARANTINED,
        )
        
        # Call client
        result = self.client.report_threat(
            device_id="dev-001",
            threat_name="Ransomware-X",
        )
        
        # Verify result
        self.assertEqual(result.threat_id, "threat-123")
        
        # Verify exact call arguments
        self.mock_stub.ReportThreat.assert_called_once()
        call_args = self.mock_stub.ReportThreat.call_args[0][0]
        self.assertEqual(call_args.device_id, "dev-001")
        self.assertEqual(call_args.threat_name, "Ransomware-X")
    
    def test_retry_on_unavailable(self):
        # Mock raises UNAVAILABLE twice, then succeeds
        self.mock_stub.ReportThreat.side_effect = [
            grpc.RpcError(
                grpc.StatusCode.UNAVAILABLE,
                "Unavailable"),
            grpc.RpcError(
                grpc.StatusCode.UNAVAILABLE,
                "Unavailable"),
            pb.ThreatResponse(threat_id="threat-456"),
        ]
        
        result = self.client.report_threat(
            device_id="dev-002",
            threat_name="Trojan-Y",
        )
        
        # Verify 3 calls were made
        self.assertEqual(
            self.mock_stub.ReportThreat.call_count, 3)
        self.assertEqual(result.threat_id, "threat-456")
    
    def test_non_retryable_error(self):
        # INVALID_ARGUMENT should not be retried
        self.mock_stub.ReportThreat.side_effect = grpc.RpcError(
            grpc.StatusCode.INVALID_ARGUMENT,
            "Invalid device ID",
        )
        
        with self.assertRaises(RuntimeError):
            self.client.report_threat(
                device_id="", threat_name="Malware")
        
        # Verify only 1 call (no retry)
        self.mock_stub.ReportThret.assert_called_once()

Example 3: Fake Server for Integration Tests

class FakeThreatServer {
  constructor() {
    this.threats = [];
    this.shouldFail = false;
    this.failCount = 0;
    this.callHistory = [];
  }
  
  start() {
    this.server = new grpc.Server();
    this.server.addService(ThreatService.service, {
      ReportThreat: (call, callback) => {
        this.callHistory.push({
          method: 'ReportThreat',
          request: call.request,
          timestamp: Date.now(),
        });
        
        if (this.shouldFail && this.failCount > 0) {
          this.failCount--;
          callback({ code: grpc.status.UNAVAILABLE, details: 'Simulated failure' });
          return;
        }
        
        const threat = {
          threatId: `threat-${this.threats.length + 1}`,
          name: call.request.name,
          status: 'QUARANTINED',
        };
        this.threats.push(threat);
        callback(null, threat);
      },
      
      ListThreats: (call, callback) => {
        callback(null, { threats: this.threats });
      },
    });
    
    return new Promise((resolve, reject) => {
      this.server.bindAsync('0.0.0.0:0',
        grpc.ServerCredentials.createInsecure(),
        (err, port) => {
          if (err) return reject(err);
          this.port = port;
          this.server.start();
          resolve(port);
        }
      );
    });
  }
  
  stop() {
    this.server.forceShutdown();
  }
}

// Test using fake server
describe('ThreatClient with Fake Server', () => {
  it('should retry and succeed after failures', async () => {
    const fake = new FakeThreatServer();
    const port = await fake.start();
    fake.shouldFail = true;
    fake.failCount = 2;
    
    const client = new ThreatClient(`localhost:${port}`);
    const result = await client.reportThreat('dev-1', 'Malware');
    
    assert.strictEqual(result.threatId, 'threat-1');
    assert.strictEqual(fake.callHistory.length, 3);
    fake.stop();
  });
});

Common Mistakes

  1. Using mocks that are too permissive — a mock that accepts any arguments can hide bugs. Use strict matching to verify exact inputs.
  2. Not testing error paths — mocks make it trivial to simulate errors. Test every gRPC status code your client handles.
  3. Mocking the wrong layer — mock at the gRPC client boundary, not the protobuf message level. Test the client's retry, timeout, and error handling.
  4. Leaking mock implementation details into tests — tests should describe what the server returns, not how the mock is configured internally.
  5. Not cleaning up fake servers — fakes that bind to ports can leak between tests if not stopped. Always use before/after hooks for cleanup.

Practice Questions

  1. What is the difference between a mock, a fake, and a stub in testing?
  2. How do you generate gRPC client mocks from protobuf definitions?
  3. Why should you test retry behavior with mocks?
  4. How do you verify call arguments with gomock?
  5. What are the advantages of fake servers over mocks for integration tests?

Challenge: Build a mock that simulates a bidirectional streaming service for threat alerts. The mock should: accept a stream of acknowledgments, yield a configurable sequence of alert messages, simulate network failures mid-stream, and verify all messages were sent in order.

Mini Project

Create a gRPC mock library with: generated mock stubs for all service methods, configurable error injection for each status code, call counting and argument verification, fake server that runs on an in-memory transport, and test helpers for retry, timeout, and streaming scenarios.

FAQ

Should I use generated mocks or hand-written fakes?

Generated mocks (gomock, mockgen) are best for unit tests. Hand-written fakes are better for integration tests where you need realistic behavior.

How do I mock streaming RPCs?

In gomock, return a mock stream object that implements the stream interface. Control what data it yields and when it errors.

Can I use mocks to test server implementations?

Yes. Mock the client that the server calls for downstream services. Verify the server calls the mock with the expected arguments.

How do I mock context deadlines?

Create a context with a very short timeout (1ms) and verify the call returns DEADLINE_EXCEEDED. The mock doesn't need to check the deadline itself.

What is the risk of over-mocking?

Over-mocking tests implementation details instead of behavior. Mock at external boundaries only (service clients) and test internal logic directly.

What's Next

Learn more about gRPC testing

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro