diff --git a/Documentation/gomock-example.md b/Documentation/gomock-example.md new file mode 100644 index 00000000..b272b0dc --- /dev/null +++ b/Documentation/gomock-example.md @@ -0,0 +1,121 @@ +# Mocking Service for gRPC + +Example code: examples/helloworld/mock + +## Why? + +To test client-side logic without the overhead of connecting to a real server. Mocking enables users to write light-weight unit tests to check functionalities on client-side without invoking RPC calls to a server. + +## Idea: Mock the client stub that connects to the server. + +We use Gomock to mock the client interface (in the generated code) and programmatically set its methods to expect and return pre-determined values. This enables users to write tests around the client logic and use this mocked stub while making RPC calls. + +## How to use Gomock? + +Documentation on Gomock can be found [here](https://github.com/golang/mock). +A quick reading of the documentation should enable users to follow the code below. + +Consider a gRPC service based on following proto file: + +```proto +//helloworld.proto + +package helloworld; + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string name = 1; +} + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} +``` + +The generated file helloworld.pb.go will have a client interface for each service defined in the proto file. This interface will have methods corresponding to each rpc inside that service. + +```Go +type GreeterClient interface { + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) +} +``` + +The generated code also contains a struct that implements this interface. + +```Go +type greeterClient struct { + cc *grpc.ClientConn +} +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error){ + // ... + // gRPC specific code here + // ... +} +``` + +Along with this the generated code has a method to create an instance of this struct. +```Go +func NewGreeterClient(cc *grpc.ClientConn) GreeterClient +``` + +The user code uses this function to create an instance of the struct greeterClient which then can be used to make rpc calls to the server. +We will mock this interface GreeterClient and use an instance of that mock to make rpc calls. These calls instead of going to server will return pre-determined values. + +To create a mock we’ll use [mockgen](https://github.com/golang/mock#running-mockgen). +From the directory ``` examples/helloworld/mock/ ``` run ``` mockgen google.golang.org/grpc/examples/helloworld/helloworld GreeterClient > mock_helloworld/hw_mock.go ``` + +Notice that in the above command we specify GreeterClient as the interface to be mocked. + +The user test code can import the package generated by mockgen along with library package gomock to write unit tests around client-side logic. +```Go +import "github.com/golang/mock/gomock" +import hwmock "google.golang.org/grpc/examples/helloworld/mock/mock_helloworld" +``` + +An instance of the mocked interface can be created as: +```Go +mockGreeterClient := hwmock.NewMockGreeterClient(ctrl) +``` +This mocked object can be programmed to expect calls to its methods and return pre-determined values. For instance, we can program mockGreeterClient to expect a call to its method SayHello and return a HelloReply with message “Mocked RPC”. + +```Go +mockGreeterClient.EXPECT().SayHello( + gomock.Any(), // expect any value for first parameter + gomock.Any(), // expect any value for second parameter +).Return(&helloworld.HelloReply{Message: “Mocked RPC”},nil) +``` + +gomock.Any() indicates that the parameter can have any value or type. We can indicate specific values for built-in types with gomock.Eq(). +However, if the test code needs to specify the parameter to have a proto message type, we can replace gomock.Any() with an instance of a struct that implements gomock.Matcher interface. + +```Go +type rpcMsg struct { + msg proto.Message +} + +func (r *rpcMsg) Matches(msg interface{}) bool { + m, ok := msg.(proto.Message) + if !ok { + return false + } + return proto.Equal(m, r.msg) +} + +func (r *rpcMsg) String() string { + return fmt.Sprintf("is %s", r.msg) +} + +... + +req := &helloworld.HelloRequest{Name: "unit_test"} +mockGreeterClient.EXPECT().SayHello( + gomock.Any(), + &rpcMsg{msg: req}, +).Return(&helloworld.HelloReply{Message: "Mocked Interface"}, nil) +``` + + +