183 lines
6.6 KiB
Markdown
183 lines
6.6 KiB
Markdown
# Mocking Service for gRPC
|
||
|
||
[Example code unary RPC](https://github.com/grpc/grpc-go/tree/master/examples/helloworld/mock_helloworld)
|
||
|
||
[Example code streaming RPC](https://github.com/grpc/grpc-go/tree/master/examples/route_guide/mock_routeguide)
|
||
|
||
## 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/ ``` 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_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)
|
||
```
|
||
|
||
## Mock streaming RPCs:
|
||
|
||
For our example we consider the case of bi-directional streaming RPCs. Concretely, we'll write a test for RouteChat function from the route guide example to demonstrate how to write mocks for streams.
|
||
|
||
RouteChat is a bi-directional streaming RPC, which means calling RouteChat returns a stream that can __Send__ and __Recv__ messages to and from the server, respectively. We'll start by creating a mock of this stream interface returned by RouteChat and then we'll mock the client interface and set expectation on the method RouteChat to return our mocked stream.
|
||
|
||
### Generating mocking code:
|
||
Like before we'll use [mockgen](https://github.com/golang/mock#running-mockgen). From the `examples/route_guide` directory run: `mockgen google.golang.org/grpc/examples/route_guide/routeguide RouteGuideClient,RouteGuide_RouteChatClient > mock_route_guide/rg_mock.go`
|
||
|
||
Notice that we are mocking both client(`RouteGuideClient`) and stream(`RouteGuide_RouteChatClient`) interfaces here.
|
||
|
||
This will create a file `rg_mock.go` under directory `mock_route_guide`. This file contins all the mocking code we need to write our test.
|
||
|
||
In our test code, like before, we import the this mocking code along with the generated code
|
||
|
||
```go
|
||
import (
|
||
rgmock "google.golang.org/grpc/examples/route_guide/mock_routeguide"
|
||
rgpb "google.golang.org/grpc/examples/route_guide/routeguide"
|
||
)
|
||
```
|
||
|
||
Now conside a test that takes the RouteGuide client object as a parameter, makes a RouteChat rpc call and sends a message on the resulting stream. Furthermore, this test expects to see the same message to be received on the stream.
|
||
|
||
```go
|
||
var msg = ...
|
||
|
||
// Creates a RouteChat call and sends msg on it.
|
||
// Checks if the received message was equal to msg.
|
||
func testRouteChat(client rgb.RouteChatClient) error{
|
||
...
|
||
}
|
||
```
|
||
|
||
We can inject our mock in here by simply passing it as an argument to the method.
|
||
|
||
Creating mock for stream interface:
|
||
|
||
```go
|
||
stream := rgmock.NewMockRouteGuide_RouteChatClient(ctrl)
|
||
}
|
||
```
|
||
|
||
Setting Expectations:
|
||
|
||
```go
|
||
stream.EXPECT().Send(gomock.Any()).Return(nil)
|
||
stream.EXPECT().Recv().Return(msg, nil)
|
||
```
|
||
|
||
Creating mock for client interface:
|
||
|
||
```go
|
||
rgclient := rgmock.NewMockRouteGuideClient(ctrl)
|
||
```
|
||
|
||
Setting Expectations:
|
||
|
||
```go
|
||
rgclient.EXPECT().RouteChat(gomock.Any()).Return(stream, nil)
|
||
```
|