81. Unit Testing for gRPC Handlers
title: “81. Unit Testing for gRPC Handlers: A Complete Guide for Engineers” description: Learn the strategies, code examples, and best practices for writing high-quality unit tests for gRPC handlers in modern backend services. date: 2024-06-12 author: Seno Adi Prasetyo tags:
- grpc
- unit-testing
- backend
- software-engineering
- go
gRPC has become the backbone of many modern backend applications. It’s no surprise: its high performance and type-safe system make gRPC a popular choice for building microservices and server-to-server communication. But the stability of a service is not determined by performance alone—it also depends on the quality of how we test our code. One of the most vital forms of testing is the unit test, especially for gRPC handlers.
In this article, I’ll explain how to implement effective unit tests for gRPC handlers, complete with a case study using Go. I’ll also provide a comparison table of testing strategies, a simulation, and a flow diagram using mermaid that you can adapt to other languages.
Why Do gRPC Handlers Need Unit Tests?
A gRPC handler is the entry point for all client request logic. That’s where validation, business rules, and interactions with external dependencies all come together. Without unit tests, even the smallest bug can cause downtime and turn into a nightmare for the entire team.
Here are some of the main reasons why unit tests for gRPC handlers matter:
| Reason | Explanation |
|---|---|
| Input Validation | Ensures incoming requests are properly validated |
| Business Logic Simulation | Tests business logic before it’s actually wired up to a database/other service |
| Regression Proof | Prevents old bugs from reappearing after a code update |
| Implicit Documentation | Good test cases = another way to read a handler’s business flow |
| Confidence to Refactor | Reduces the risk of changing code |
Anatomy of a Simple gRPC Handler
Let’s look at a simple example of a gRPC handler written in Go.
1// protos/user.proto
2service UserService {
3 rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
4}
5
6message GetUserRequest {
7 string id = 1;
8}
9
10message GetUserResponse {
11 string id = 1;
12 string name = 2;
13 string email = 3;
14}The handler looks roughly like this:
1// internal/handler/user.go
2type UserRepository interface {
3 FindByID(ctx context.Context, id string) (*User, error)
4}
5
6type UserServiceHandler struct {
7 repo UserRepository
8}
9
10func (h *UserServiceHandler) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
11 user, err := h.repo.FindByID(ctx, req.Id)
12 if err != nil {
13 return nil, status.Errorf(codes.NotFound, "user not found")
14 }
15 return &pb.GetUserResponse{
16 Id: user.ID,
17 Name: user.Name,
18 Email: user.Email,
19 }, nil
20}Note: The example above already separates the dependency (UserRepository) to make it easier to test.
Strategies for Unit Testing gRPC Handlers
When writing unit tests for gRPC handlers, dependencies should be mocked. Don’t actually query the database or call an external service.
Handler Testing Flow Diagram
flowchart TD
A[Setup Test] --> B[Mock Dependensi]
B --> C[Panggil Handler dengan Request]
C --> D[Verifikasi Response & Error]
D --> E[Bersihkan dan Selesai]
Testing Packages You Can Use
| Language | Test Package | Mock Package |
|---|---|---|
| Go | testing | github.com/stretchr/testify/mock, gomock |
| Java | JUnit | Mockito |
| Python | unittest | unittest.mock |
| Node | jest | sinon |
For Go, we’ll use testify to keep the code clean and easy to read.
Example of a gRPC Handler Unit Test (Go)
Creating a Mock Repository
1// internal/repository/user_mock.go
2import "github.com/stretchr/testify/mock"
3
4type MockUserRepository struct {
5 mock.Mock
6}
7
8func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
9 args := m.Called(ctx, id)
10 if user, ok := args.Get(0).(*User); ok {
11 return user, args.Error(1)
12 }
13 return nil, args.Error(1)
14}Writing the Unit Test for the Handler
1// internal/handler/user_test.go
2import (
3 "context"
4 "testing"
5 "github.com/stretchr/testify/assert"
6 "yourproject/internal/repository"
7 pb "yourproject/protos"
8)
9
10func TestGetUser_Success(t *testing.T) {
11 // Setup
12 mockRepo := new(repository.MockUserRepository)
13 handler := &UserServiceHandler{repo: mockRepo}
14 testUser := &User{ID: "1", Name: "Budi", Email: "budi@email.com"}
15
16 // Arrange
17 mockRepo.On("FindByID", mock.Anything, "1").Return(testUser, nil)
18
19 // Act
20 req := &pb.GetUserRequest{Id: "1"}
21 resp, err := handler.GetUser(context.Background(), req)
22
23 // Assert
24 assert.NoError(t, err)
25 assert.NotNil(t, resp)
26 assert.Equal(t, "1", resp.Id)
27 assert.Equal(t, "Budi", resp.Name)
28 assert.Equal(t, "budi@email.com", resp.Email)
29 mockRepo.AssertExpectations(t)
30}
31
32func TestGetUser_NotFound(t *testing.T) {
33 mockRepo := new(repository.MockUserRepository)
34 handler := &UserServiceHandler{repo: mockRepo}
35
36 mockRepo.On("FindByID", mock.Anything, "2").Return(nil, errors.New("not found"))
37
38 req := &pb.GetUserRequest{Id: "2"}
39 resp, err := handler.GetUser(context.Background(), req)
40
41 assert.Error(t, err)
42 assert.Nil(t, resp)
43 assert.Contains(t, err.Error(), "user not found")
44 mockRepo.AssertExpectations(t)
45}Simulating Test Execution
With the code above, the tests will run like this:
1=== RUN TestGetUser_Success
2--- PASS: TestGetUser_Success (0.00s)
3=== RUN TestGetUser_NotFound
4--- PASS: TestGetUser_NotFound (0.00s)
5PASS
6ok yourproject/internal/handler 0.005sBest Practices for Unit Testing gRPC Handlers
Here are some important best practices to apply:
- Isolate Dependencies
- Use the interface & mock pattern so the handler can be tested in isolation from the DB/external services.
- Test Every Scenario
- Cover success, not found, invalid input, and other error branches.
- Minimize Side Effects
- The handler should ideally be pure: change external state as little as possible within the scope of the test.
- Be Explicit About Expectations
- Check the returned error, the message, and the mock calls in detail.
- Automate in CI
- Make sure all unit tests run automatically in your CI/CD pipeline.
When Should You Use Integration Tests or E2E?
Unit tests only verify logic in isolation. If you want to test the entire flow (handler, network, database), use integration tests or E2E tests.
Still, unit tests for handlers remain a must have because they execute quickly and the cost of finding a bug with them is cheap.
Conclusion
Unit testing gRPC handlers isn’t just about increasing coverage—it’s about the reliability of your service. With a strategy of dependency isolation, mocking, and broad scenario coverage, your engineering team will have the peace of mind to develop new features without fear that old bugs will suddenly resurface.
I hope that with the examples, simulation, and diagrams above, you’ll feel even more confident writing unit tests for gRPC handlers in your next project 🚀
References
Happy testing!