cuelangを使ったgrpcのテストツールを作った話


本記事はGo3 Advent Calendar 2019 17日目の記事です。

背景

少し前(9月頃?)にGoogleからcueというschemaベースでコンフィグファイルなどに使えるデータ形式が発表されました。相変わらずググラビリティの低い名前ですが、jsonやymlのスーパーセットでありながら、型定義が書けたりvalidationができたりする面白いやつです。

面白いやつなので何か使えないかなーと考えていると、型定義が使えるならgRPCとかと相性が良さそうだなと思いつきました。これはGoogleでも考えられていたのか、cueの実装にはprotobufからcueファイルを生成する実装が含まれています。

ちょうど仕事で毎回 grpcurl への入力をコピペするのにも飽きていたので、この辺りを自動化するテストツールを作り始めました。

ツールの紹介と使い方

まだ出来立てで実戦に投入していませんが、grpc-testing というツールを作成しました。go getでインストールしてください。

go get -u github.com/ryoya-fujimoto/grpc-testing

またリポジトリにはexample/以下にサンプル用のprotobufファイルとgRPCサーバーの実装が入っているので、動かしながら使い方を説明していきます。

git clone https://github.com/ryoya-fujimoto/grpc-testing.git
cd grpc-testing/example/app
go run main.go

テストファイルの作成

grpc-testing addを使うとprotobuf定義からテスト用のファイルを生成できます。例えば以下のようにprotobufファイルを指定すると、次のようなcueファイルが生成されます。

grpc-testing add --proto_path example/app --protofiles example/app/*.proto FirstTest
{
    name: "FirstTest"
    Input: {
    }
    Output: {
    }
    Test :: {
        method: string
        input:  Input
        output: Output
    }
    cases: [...Test] & [{
        method: ""
        input: {
        }
        output: {
        }
    }]
    GetUserRequest: {
        id?: uint64 @protobuf(1)
    }
    CreateUserRequest: {
        name?: string @protobuf(1)
    }
    User: {
        name?: string @protobuf(2)
        id?:   uint64 @protobuf(1)
    }
}

GetUserRequestUserはprotobufに従って生成された型定義です。この型定義を使ってgRPCのinputやoutputを記述することで、schema定義の恩恵に預かることができます。

casesがテスト本体を記述する変数で、methodにはgRPCのメソッド名を、inputとoutputにはサーバーへのリクエストとレスポンスデータを記述します。

casesはschema定義の恩恵に預かりながら以下のように記述することができます。

cases: [...Test] & [{
  method: "UserService.GetUser"
  input: GetUserRequest & {
    id: 5
  }
  output: {
    id: "5"
    name: "John Smith"
  }
}]

(outputに型定義を使っていないのは後述の理由によるものです)

値のvalidation

せっかくcueで設定を書いているのでvalidationがしたくなります。grpc-testing validateをするとテストファイルの内容を検査してくれます。

$ grpc-testing validate
OK: tests/addTest.cue
OK: tests/firstTest.cue

上の例でid: "5"のような型定義に違反した記述をすると、validationが失敗します。

$ grpc-testing validate
OK: tests/addTest.cue
NG: tests/firstTest.cue
conflicting values (int & >=0 & int & <=18446744073709551615) and "5" (mismatched types int and string)

サーバーにリクエストを投げる

テストの前にはとりあえずサーバーにリクエストを投げてみたくなります。grpc-testing runを実行すると、ライブラリとして使っているgrpcurlを使ってinputの内容をサーバーにリクエストします。

$ grpc-testing run localhost:8080 FirstTest
output: {
  "id": "5",
  "name": "John Smith"
}

レスポンスをテストする

いよいよテストです。上述の通りoutputにはレスポンスとして受け取るべき値を設定しておきます。

cases: [...Test] & [{
  method: "UserService.GetUser"
  input: GetUserRequest & {
    id: 5
  }
  output: {
    id: "5"
    name: "John Smith"
  }
}]

テストを実行するのはgrpc-testing testコマンドです。

$ grpc-testing test localhost:8080 FirstTest
OK: FirstTest

味気ないですが、outputを少し変更すると以下のように失敗します。

$ grpc-testing test localhost:8080 FirstTest
NG: FirstTest
expect: {"id":"5","name":"John Hoge"}, but: {"id":"5","name":"John Smith"}

現状grpcurlの使い方が甘いのか、レスポンスのjsonを型定義に従った値にできていません。
(レスポンスのidが文字列になってしまっている。)
サーバーからの送信時はuint64なのに何故・・・。

この辺り今後詰めていきたいと思います。

終わりに

実践投入もまだですし、grpcurlの使い方も中途半端なので現実のユースケースに対応できている状態ではありません。まだReflectionを実装しているサーバーを想定していますし、実務で使うような複数のprotobufファイルを使ったテストファイルの生成もうまくいくか分かりません。

outputの問題もそうですが、例えばmethodの型定義を"UserService.GetUser" |
"UserService.CreateUser"
のようにするなど、もう少しprotobuf由来の情報が使えないかなと思います。
あとはschema定義とテストファイルを別ファイルに分けるなどしたかったのですが、cueのパッケージ管理(?)周りが謎すぎて挫折しました。

色々と課題はありますが、それでもcueの良さを生かしつつ、仕事が少し楽しくなるようなツールができたかなと思います。

追記

こっそり改名したので修正