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