84. Mocking a gRPC Server with `gomock`
title: “84. Mocking a gRPC Server with gomock”
author: “Senior Software Engineer”
date: 2024-06-26
Mocking a gRPC Server with gomock
In modern software development, testing has become one of the main pillars for maintaining software quality, especially in systems that rely on a microservices architecture and RPC (Remote Procedure Call) communication such as gRPC. One of the challenges in testing a gRPC-based service layer is figuring out how to mock the server on the other end of the conversation. Fortunately, the Go ecosystem already provides a powerful tool called gomock
. In this article, I’ll walk through in detail how to use gomock to build a mock gRPC server, complete with code examples, simulations, and flow diagrams to help you visualize everything.
Why Do We Need to Mock a gRPC Server?
Before diving into the technical details, let’s first understand the following scenario:
Imagine we are building a Service A that communicates with a Service B using gRPC. Service B is a third-party service or is still being developed by another team. If we want to test Service A in isolation, calling Service B directly would make the test flaky (since it depends on Service B), slow down execution, and be hard to control.
The solution? If we can mock the gRPC server, then we can:
- Get responses that match the scenarios we want (happy/edge cases).
- Measure how much of the logic in Service A is covered without depending on Service B.
- Run tests faster and more reliably.
An Overview of the Testing Flow with Mocks
Using mocks with gRPC can be described with a simple flow like the following:
flowchart LR
TestCase[Pengujian (unit/integration)]
MockServer[Mock gRPC Server (gomock)]
ServiceA[Service A (Client)]
ServiceA --Request--> MockServer
MockServer --Response--> ServiceA
TestCase --Assertion/Validation--> ServiceA
In the diagram above, the TestCase builds a mock server and then injects it into Service A. Every time Service A makes a request, the mock server responds according to what was configured in the test scenario.
Project Structure
To make it more realistic, I’ll use a simple case study:
- Define the gRPC protoc file (for example, a
UserService) - Generate Go code from the Protobuf
- Create a mock interface with
gomock - Implement a unit test using the mock server
Here is an example of a simple directory structure:
1.
2├── proto/
3│ └── user.proto
4├── pb/
5│ └── user.pb.go
6├── internal/
7│ ├── service.go
8│ └── service_test.go
9└── go.mod1. Define the Protobuf File
For example, the file proto/user.proto:
1syntax = "proto3";
2package pb;
3
4service UserService {
5 rpc GetUser(GetUserRequest) returns (GetUserResponse);
6}
7
8message GetUserRequest {
9 string id = 1;
10}
11
12message GetUserResponse {
13 string id = 1;
14 string name = 2;
15}2. Generate the gRPC Stub
Generate the Go code:
1protoc --go_out=pb --go-grpc_out=pb proto/user.protoAs a result, the pb/ folder will contain user.pb.go and user_grpc.pb.go, which hold the client & server interfaces (for UserService):
1// The server interface that will be mocked
2type UserServiceServer interface {
3 GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
4}3. Generate the Mock Interface with gomock
Install mockgen from gomock if you don’t have it yet:
1go install github.com/golang/mock/mockgen@latestGenerate the mock for the server interface:
1mockgen -source=pb/user_grpc.pb.go -destination=internal/mock_user.go -package=internalThe file internal/mock_user.go will contain the MockUserServiceServer struct if you want to mock the server.
4. Implement a Unit Test with the Mock
Suppose we have the following logic in internal/service.go:
1package internal
2
3import (
4 "context"
5 "pb"
6)
7
8type Handler struct {
9 userService pb.UserServiceClient
10}
11
12func NewHandler(client pb.UserServiceClient) *Handler {
13 return &Handler{userService: client}
14}
15
16func (h *Handler) FindUser(ctx context.Context, id string) (string, error) {
17 resp, err := h.userService.GetUser(ctx, &pb.GetUserRequest{Id: id})
18 if err != nil {
19 return "", err
20 }
21 return resp.Name, nil
22}Testing Simulation: Mocking the gRPC Server Client
The unit test scenario:
- We want to make sure that when the
FindUserservice callsGetUser, we can control the response (for example, name=“John Doe”) and test our logic without actually calling the real server.
Here is an example of the code in internal/service_test.go:
1package internal
2
3import (
4 "context"
5 "testing"
6
7 "github.com/golang/mock/gomock"
8 "pb"
9)
10
11func TestFindUser(t *testing.T) {
12 ctrl := gomock.NewController(t)
13 defer ctrl.Finish()
14
15 mockUserSrv := NewMockUserServiceClient(ctrl)
16
17 // Setup mock: whenever GetUser is called with id '123', return John Doe
18 mockUserSrv.EXPECT().
19 GetUser(gomock.Any(), &pb.GetUserRequest{Id: "123"}).
20 Return(&pb.GetUserResponse{Id: "123", Name: "John Doe"}, nil)
21
22 handler := NewHandler(mockUserSrv)
23
24 name, err := handler.FindUser(context.Background(), "123")
25 if err != nil {
26 t.Fatalf("unexpected error: %v", err)
27 }
28 if name != "John Doe" {
29 t.Fatalf("expected John Doe, got %v", name)
30 }
31}Table: Comparing Testing Without a Mock vs. With a Mock
| Method | Advantages | Disadvantages |
|---|---|---|
| Without a mock | Real end-to-end, catches bugs between services | Slow, fragile, hard to set up |
With gomock | Fast, isolated, deterministic | Doesn’t cover real server bugs |
Best Practices for Mocking gRPC with gomock
Mock only what’s necessary
Mock only the behavior that is a dependency, not the SUT (System Under Test) itself.Use .EXPECT() as expressively as possible
To know exactly which parameters are used and how many times something is called, use .AnyTimes(), .Times(), or other matchers fromgomock.Separate mock logic into helper functions
If a test is fairly complex, reuse the mock setup in a validation helper for cleaner code.
When Is Mocking the Server Better Than an Integration Test?
Use a mock when:
- Writing unit tests for a logic component that interacts with a dependency via gRPC.
- You want fast and repeatable tests.
Use a real integration test when:
- You want to make sure the services are truly compatible with each other.
- You are getting close to staging/production.
Conclusion
Mocking a gRPC server with gomock makes the Go testing workflow far more robust, fast, and maintainable. By generating mocks from the Protobuf interface, developers can easily test the capabilities of the client/service layer without worrying about the complexity and instability of an external server. This is a must-have skill for any Go engineer working with distributed systems.
References
Happy coding! Don’t hesitate to explore testing your distributed systems more flexibly using mocks!