シナリオテスティングツール/パッケージであるrunnをgRPCに対応させた

なんとかgRPC対応ができました。

「そもそもrunnって何?」については会社のテックブログにエントリを書きましたのでそちらをご覧ください。

tech.pepabo.com

なお、runnを使ったgRPCのシナリオテストの実戦投入は私もまだなので、もし触ってくださる方がいましたら是非フィードバックをお待ちしています。

gRPC Runner

runnでシナリオの各ステップを実行するコンポーネントをRunnerと呼んでいるのですが、今回gRPC Runnerを追加しました。

runners: セクションで grpc:// というschemeでgRPC Runnerを作成できるようにしています。

次の例だと greq という名前をつけたgRPC Runnerを作成しています。

runners:
  greq: grpc://grpc.example.com:80

443ポートにするとTLSを使用するようになります。細かく設定する場合は以下のように書けます。

runners:
  greq:
    addr: grpc.example.com:8080
    tls: true
    cacert: path/to/cacert.pem
    cert: path/to/cert.pem
    key: path/to/key.pem
    # skipVerify: false

私が主に想定しているユースケースが「開発しているgRPCサーバのシナリオテスト」なので、大抵は次のようなコードで runners: セクションの設定を上書きします(次の例だと greq を runn.Runner() で上書きしています)。

func TestServer(t *testing.T) {
    addr := "127.0.0.1:8080"
    l, err := net.Listen("tcp", addr)
    if err != nil {
        t.Fatal(err)
    }
    ts := grpc.NewServer()
    myapppb.RegisterMyappServiceServer(s, NewMyappServer())
    reflection.Register(s)
    go func() {
        s.Serve(l)
    }()
    t.Cleanup(func() {
        ts.GracefulStop()
    })
    opts := []runn.Option{
        runn.T(t),
        runn.Runner("greq", fmt.Sprintf("grpc://%s", addr),
    }
    o, err := runn.Load("testdata/books/**/*.yml", opts...)
    if err != nil {
        t.Fatal(err)
    }
    if err := o.RunN(ctx); err != nil {
        t.Fatal(err)
    }
}

gRPCリクエストの書き方

4種類の通信方式はそれぞれ次のようにかけます。

なお、protoファイルは https://github.com/k1LoW/runn/blob/main/testdata/grpctest.proto を想定しています。

Unary RPC

headers: と message:` でリクエストを投げることができます。レスポンスはHTTP Runnerと同様に自動で記録され使用できます。

steps:
  hello_unary:
    desc: Request using Unary RPC
    greq:
      grpctest.GrpcTestService/Hello:
        headers:
          authentication: tokenhello
        message:
          name: alice
          num: 3
          request_time: 2022-06-25T05:24:43.861872Z
    test: |
      steps.hello_unary.res.status == 0 && steps.hello_unary.res.message.num == 3

Server streaming RPC

Server streaming RPCもUnary RPCと同様に headers:message: でリクエストを投げることができます。

レスポンスは複数のメッセージが返ってくることがあるため steps.<step_id>.res.messages に配列でレスポンスメッセージが記録されます。

steps:
  hello_server_streaming:
    desc: Request using Server streaming RPC
    greq:
      grpctest.GrpcTestService/ListHello:
        headers:
          authentication: tokenlisthello
        message:
          name: bob
          num: 4
          request_time: 2022-06-25T05:24:43.861872Z
    test: |
      steps.hello_server_streaming.res.status == 0 && len(steps.hello_server_streaming.res.messages) == 2 && steps.hello_server_streaming.res.messages[1].num == 34

Client streaming RPC

Client streaming RPCは複数のメッセージを送るので message: の代わりに messages: セクションで複数のリクエストを投げることができます。

steps:
  hello_client_streaming:
    desc: Request using Client streaming RPC
    greq:
      grpctest.GrpcTestService/MultiHello:
        headers:
          authentication: tokenmultihello
        messages:
          -
            name: alice
            num: 5
            request_time: 2022-06-25T05:24:43.861872Z
          -
            name: bob
            num: 6
            request_time: 2022-06-25T05:24:43.861872Z
    test: |
      steps.hello_client_streaming.res.status == 0 && steps.hello_client_streaming.res.message.num == 35

Bidirectional streaming RPC

同期的にではありますがBidirectional streaming RPCもサポートしています。 messages: セクションで receive キーワードでサーバからのメッセージを1つ受信し、close キーワードで通信を閉じます。

  hello_bidi_streaming:
    desc: Request using Bidirectional streaming RPC
    greq:
      grpctest.GrpcTestService/HelloChat:
        headers:
          authentication: tokenhellochat
        messages:
          -
            name: alice
            num: 7
            request_time: 2022-06-25T05:24:43.861872Z
          - recieve # recieve server message
          -
            name: bob
            num: 8
            request_time: 2022-06-25T05:24:43.861872Z
          -
            name: charlie
            num: 9
            request_time: 2022-06-25T05:24:43.861872Z
          - close # close connection
    test: |
      steps.hello_bidi_streaming.res.status == 0 && len(steps.hello_bidi_streaming.res.messages) == 1

gRPCサポートを終えて

まだ実戦投入していないのでアレですが、これでrunnで最低限欲しいと思っていた機能がひと通り揃いました。

あとはテックブログに書いたような機能を必要性やモチベーションに応じて追加したり、細かいところをちまちまと整備したりしようかと思います*1

そう言えば、gRPCのテストを書くために grpcstub を作ったりしたのですが、それはまたどこかで紹介したいです。

なお、runnを使ったgRPCのシナリオテストの実戦投入は私もまだなので、もし触ってくださる方がいましたら是非フィードバックをお待ちしています(大事なことなので2回(ry)。

*1:そういえば Flaky なテストが残っていて(TestShardという名前のテストでYAMLのパースが時々失敗しているように見える)、それも原因を特定したい。。