mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-12-24 05:50:22 +00:00
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)
|
|||
|
```
|