gRPCのクライアントが絡むテスト
golang.tokyo #28
4 December 2019
dice_zu(daisuzu)
dice_zu(daisuzu)
自分たちが作っているgRPCサーバのクライアントとか、
実は3rd-partyパッケージの中で使われていたりとか。
1. DIする
2. ダミーサーバを使う
3. リクエストとレスポンスを記録/再生する
自分のコードは pb.SearchClient インタフェースを満たす cli を渡せればOK。
type myHandler struct{ cli pb.SearchClient }
func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// rからreqを作ったり
reply, err := h.cli.Search(ctx, req)
// エラーハンドリングしたり、replyを加工したり
}cli に手を出せない3rd-partyパッケージなどはメソッドごと差し替える必要がある。
func (c *Client) Run(ctx context.Context, q *Query) *Result {
// qを色々な処理(バリデーションとか)をしてreqに変換する
resp, err := c.cli.Search(ctx, req)
// respとerrを色々な処理をしてResult型に変換する
}その場合、 色々な処理 はテストでスキップされてしまう...
4gRPCのサーバは比較的簡単に作れる。
type fakeServer struct {
cli pb.ServiceServer // ⬅️のinterfaceに対応するメソッドを実装する
data map[string]interface{}
}type mockServer struct {
cli pb.ServiceServer // ⬅️のinterfaceに対応するメソッドを実装する
f func(context.Context, *pb.Request) (*pb.Response, error)
}あとはテスト時に接続先を変えるだけ。ただコード量は多くなりがち...
5cloud.google.com/go/rpcreplay を使う。
r, err := rpcreplay.NewRecorder("service.replay", nil)
if err != nil { ... }
defer func() {
if err := r.Close(); err != nil { ... }
}()r, err := rpcreplay.NewReplayer("service.replay")
if err != nil { ... }
defer r.Close()あとは r.DialOptions() を使ってgRPCクライアントを作るだけ。
6こういう形式になっていますよね?
func NewClient(addr string, opts ...grpc.DialOption) (pb.ServiceClient, error) {
conn, err := grpc.Dial(addr, opts...)
if err != nil {
return nil, err
}
return pb.NewServiceClient(conn), nil
}もしなっていなくても ...grpc.DialOption を足す分には既存コードに影響ないはず!
7