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

本記事は[Go3 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/go3) 17日目の記事です。

# 背景

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

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

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

# ツールの紹介と使い方

まだ出来立てで実戦に投入していませんが、[grpc-testing](https://github.com/ryoya-fujimoto/grpc-testing) というツールを作成しました。`go get`でインストールしてください。

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

また[リポジトリ](https://github.com/ryoya-fujimoto/grpc-testing)には`example/`以下にサンプル用のprotobufファイルとgRPCサーバーの実装が入っているので、動かしながら使い方を説明していきます。

```bash
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ファイルが生成されます。

```bash
grpc-testing add --proto_path example/app --protofiles example/app/*.proto FirstTest
```

```yaml
{
	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定義の恩恵に預かりながら以下のように記述することができます。

```yaml
cases: [...Test] & [{
  method: "UserService.GetUser"
  input: GetUserRequest & {
    id: 5
  }
  output: {
    id: "5"
    name: "John Smith"
  }
}]
```
(`output`に型定義を使っていないのは後述の理由によるものです）

## 値のvalidation

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

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

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

```sh
$ 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`の内容をサーバーにリクエストします。

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

## レスポンスをテストする

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

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

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

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

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

```bash
$ 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の良さを生かしつつ、仕事が少し楽しくなるようなツールができたかなと思います。

# 追記

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