How to mock a dummy grpc service with methods in Go

Viewed 1804

I need to write unit test for a function, which in its source code calling a gRPC method of a different gRPC service. My test result in panic error because that gRPC service is not running on my machine. I want to create a dummy service in my test and that service must serve the method calling inside the source code. How do I do that?

Edit: Details Source function:

func (s *mainType) MainGenerateBearerToken(inputs...)(output) {
        response, err := s.Subtype.SubGenerateBearerToken(context, authnPass, ttl)
        if err != nil {
            return nil, err
        }
        return &models.OCITokenResponse{Token: response.Token, ExpiresIn: ttl}, nil
    }

func(cl *Subtype) SubGenerateBearerToken(context,authnPass, ttl) (*BearerTokenResponse, error) {
    
    resp,err := pb.NewGrpcClient(cl.Conn).GetBearerToken_grpc(ctx, &pb.GetBearerTokenRequest{AuthnToken, BearerTtlValue:ttlNum)
    
    return &BearerTokenResponse{Token}, err
}

I'm writing test for the MainGenerateBearerToken() function, which calls SubGenerateBearerToken(), inside which the grpc method is called.

2 Answers

A gRPC service client is a regular Go interface.

You can use tools like gomock or moq to generate a mock for any Go interface.

$ go install github.com/golang/mock/mockgen@v1.6.0
$ mockgen -source=my_service.pb.go MyServiceClient

Or

$ go install github.com/matryer/moq@v0.2.5
$ moq -pkg mock -out mock/my_service.go . MyServiceClient

That will generate mock.MyServiceClientMock that implements MyServiceClient. Then you can use it in your unit tests instead of the real gRPC client.

Complete example using moq:

my_service.proto:

syntax = "proto3";

package main;
option go_package = "main";

service MyService {
    rpc Hello(HelloMesage) returns (HelloMesage) {}
}

message HelloMesage {
  string msg = 1;
}

main.go:

package main

import context "context"

//go:generate protoc --go_out=plugins=grpc:. -I=. my_service.proto
//go:generate moq -out my_service_mock.go . MyServiceClient

func CallHello(myService MyServiceClient) (*HelloMesage, error) {
    return myService.Hello(context.TODO(), &HelloMesage{Msg: "hello"})
}

func main() {
    // (omitted) call grpc normally ...
}

main_test.go:

package main

import (
    context "context"
    "testing"

    "github.com/stretchr/testify/assert"
    grpc "google.golang.org/grpc"
)

func TestCallHello(t *testing.T) {
    myServiceClient := &MyServiceClientMock{
        HelloFunc: func(ctx context.Context, in *HelloMesage, opts ...grpc.CallOption) (*HelloMesage, error) {
            return &HelloMesage{Msg: in.Msg + " reply"}, nil
        },
    }
    res, err := CallHello(myServiceClient)
    assert.NoError(t, err)
    assert.Equal(t, "hello reply", res.Msg)

}

To run:

$ go mod init myservice
$ go mod tidy
$ go generate
$ go test

If it's appropriate to get rid of the grpc client creation in the SubGenerateBearerToken method (e.g. keep it in the Subtype or pass as an arg) you could easily create a simple fake client:

type fakeClient struct {
    grpc.YourClient // a client interface from your *.pb.go file
}

func NewFakeClient(_ *grpc.ClientConn) *fakeClient {
    return &fakeClient{}
}

func (fc *fakeClient) GetBearerToken_grpc(ctx context.Context, in *pb.GetBearerTokenRequest, opts ...grpc.CallOption) (*pb.BearerTokenResponse, error) {
    return &pb.BearerTokenResponse{
        // your fake response...
    }, nil
}
Related