122 lines
4.2 KiB
Markdown
122 lines
4.2 KiB
Markdown
|
# Mocking Service for gRPC
|
|||
|
|
|||
|
[Example code](https://github.com/grpc/grpc-go/tree/master/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)
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
|