You can mock gRPC dependencies in your unit and integration tests by creating a fake implementation of your gRPC service that returns predefined responses.

Let’s see this in action with a simple Go example. Imagine you have a UserService that depends on a NotificationServiceClient to send email notifications.

// Original service that depends on gRPC client
type UserService struct {
	notificationClient NotificationServiceClient // gRPC client interface
}

func (s *UserService) RegisterUser(ctx context.Context, name string) error {
	// ... user registration logic ...

	// Call the gRPC client to send a welcome email
	err := s.notificationClient.SendEmail(ctx, &pb.SendEmailRequest{
		To:      "user@example.com",
		Subject: "Welcome!",
		Body:    "Thanks for registering!",
	})
	if err != nil {
		return fmt.Errorf("failed to send welcome email: %w", err)
	}

	return nil
}

// Mock implementation of the gRPC client interface
type MockNotificationClient struct {
	// Store expected requests and predefined responses
	SendEmailFunc func(ctx context.Context, req *pb.SendEmailRequest) (*pb.SendEmailResponse, error)
}

// Implement the gRPC client interface method
func (m *MockNotificationClient) SendEmail(ctx context.Context, req *pb.SendEmailRequest) (*pb.SendEmailResponse, error) {
	if m.SendEmailFunc != nil {
		return m.SendEmailFunc(ctx, req)
	}
	// Default behavior if SendEmailFunc is not set
	return &pb.SendEmailResponse{}, nil
}

// Example unit test
func TestUserService_RegisterUser_Success(t *testing.T) {
	mockClient := &MockNotificationClient{
		SendEmailFunc: func(ctx context.Context, req *pb.SendEmailRequest) (*pb.SendEmailResponse, error) {
			// Assert that the correct request was made
			if req.GetSubject() != "Welcome!" {
				t.Errorf("Expected subject 'Welcome!', got '%s'", req.GetSubject())
			}
			// Return a successful response
			return &pb.SendEmailResponse{}, nil
		},
	}

	userService := &UserService{
		notificationClient: mockClient,
	}

	err := userService.RegisterUser(context.Background(), "Alice")
	if err != nil {
		t.Fatalf("RegisterUser failed: %v", err)
	}
}

func TestUserService_RegisterUser_NotificationFailure(t *testing.T) {
	mockClient := &MockNotificationClient{
		SendEmailFunc: func(ctx context.Context, req *pb.SendEmailRequest) (*pb.SendEmailResponse, error) {
			// Simulate a gRPC client error
			return nil, errors.New("notification service unavailable")
		},
	}

	userService := &UserService{
		notificationClient: mockClient,
	}

	err := userService.RegisterUser(context.Background(), "Bob")
	if err == nil {
		t.Fatal("Expected an error when notification fails, but got none")
	}
	if !strings.Contains(err.Error(), "failed to send welcome email: notification service unavailable") {
		t.Errorf("Unexpected error message: %v", err)
	}
}

This pattern allows you to isolate your UserService logic. In unit tests, you provide a MockNotificationClient that behaves exactly as you define, allowing you to test success paths and error conditions of UserService without actually calling a real gRPC server. For integration tests, you might use a real, but perhaps ephemeral, gRPC server that your mocked client can then connect to, or you might still use a mock if you’re testing interactions between multiple internal services.

The core problem this solves is testability. Real gRPC services can be complex to set up and manage for every test run. They might require databases, network access, or specific configurations. By mocking, you decouple your service logic from these external dependencies, making tests faster, more reliable, and easier to write. You’re essentially replacing the real, external world with a controlled, predictable environment.

Internally, the mock implementation adheres to the same interface as the real gRPC client. This is crucial. Go’s interfaces are implicit, meaning if your MockNotificationClient has a SendEmail method with the exact signature as the NotificationServiceClient interface, it is a NotificationServiceClient. This allows you to swap the real client with the mock without changing the UserService code itself. You inject the dependency (the notificationClient) into UserService, and in your tests, you inject the mock.

The real power comes from the flexibility of the mock. You can program its behavior on a per-test basis. In TestUserService_RegisterUser_Success, the SendEmailFunc checks the request details and returns a success. In TestUserService_RegisterUser_NotificationFailure, it’s programmed to return an error. This fine-grained control lets you simulate a vast array of scenarios, including network timeouts, malformed responses, or specific business logic failures from the dependency.

A common pitfall is not mocking deeply enough. If your UserService also calls another internal service via gRPC, you need to mock that client too. Over-reliance on real dependencies in tests leads to brittle, slow test suites that are hard to debug. The goal is to test your code, not the entire distributed system at once.

When you have multiple gRPC clients your service depends on, you’ll create a mock for each. For instance, if UserService also needed to call an AuthServiceClient, you’d have MockAuthServiceClient with its own MockAuthServiceClientFunc for each method. You then inject both mocks into your UserService.

The most surprising true thing about this pattern is that you can even use it to test the behavior of the gRPC client itself in integration tests, by pointing your mock client’s function to a real, but minimal, gRPC server you spin up just for that test. This blurs the line between unit and integration testing, allowing you to verify the contract adherence between your client code and a server implementation without the full complexity of a production environment.

Next, you’ll likely want to explore how to manage these mocks effectively across larger test suites, perhaps using dependency injection frameworks or shared mock factories.

Want structured learning?

Take the full Grpc course →