本記事は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)
}
}
GetUserRequest
やUser
は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の良さを生かしつつ、仕事が少し楽しくなるようなツールができたかなと思います。
追記
こっそり改名したので修正