Closed9

Go + gRPCのシナリオテストにrunnを使用したいメモ

ぱんだぱんだ

個人的に求めているもの

  • gRPCのシナリオテストを実行したい
  • 単体テスト、インテグレーションテストレベルであればGoのテストコードとして実行がいいけど、複数のgRPCサービスを組み合わせた時のテストをテストコードで書くのは辛い気がする
  • なるべくシンプルにシナリオが書ける方がいい
  • CIにのせることも考えたいのでCLIで実行できるようにしたい
ぱんだぱんだ

候補

  • runn シナリオをyamlファイルで書ける。CLIでもGoのテストコードとしても実行できる。
  • scenarigo
  • postmanいつのまにか正式リリースになっていた。JavaScriptでスクリプトを実行できる。ドキュメントはあるけど日本語の情報や実際のシナリオ例などが少なすぎる。
  • datadog DatadogでもgRPCのe2eテストできるみたいなのを見かけた気がしたので。調べたところヘルスチェックブラウザテストが使えそうだけど求めているものとは違う。

他にもありそうだけどこの中だとrunnかscenarigoが良さそうだけどrunnの方がドキュメントが充実していそう(zenn本公開している。有料だけど8割くらい無料公開されている。READMEもかなり書いてある。)なのでrunnを触ってみる。

ぱんだぱんだ

install

brewとかでもいけるようだけど今回はgo install。

go install github.com/k1LoW/runn/cmd/runn@latest

runn --version
> runn version 0.67.0
ぱんだぱんだ

grpcurlからランブックを作成

runn new -- grpcurl --d '{"name": "World"}' localhost:8080 hello.GreetingService.Hello

desc: Generated by `runn new`
runners:
  greq: grpc://localhost:8080
steps:
- greq:
    hello.GreetingService.Hello:
      message:
        name: World

ランブックを作成して実行し、テストまで作成する

runn new --and-run --grpc-no-tls -- grpcurl -d '{"name": "World"}' localhost:8080 
hello.GreetingService/Hello

desc: Generated by `runn new`
runners:
  greq: grpc://localhost:8080
steps:
- greq:
    hello.GreetingService/Hello:
      message:
        name: World
  test: |
    current.res.headers['content-type'][0] == "application/grpc"
    && compare(current.res.message, {"message":"Hello, World!"})
    && current.res.status == 0
ぱんだぱんだ

ファイルの作成、ステップの追加

runn new --and-run --grpc-no-tls --out ./runbooks/runbook.yml -- grpcurl -d '{"name": "World"}' localhost:8080 hello.GreetingService/Hello

--outオプションで出力ファイルを指定。なければ作成するし既に存在していればステップを追記する。

ぱんだぱんだ

テストコードとして実行

"github.com/k1LoW/runn"パッケージをインストールしてテストコードとして実行できる。

func TestHello(t *testing.T) {
	addr := "127.0.0.1:8080"
	l, err := net.Listen("tcp", addr)
	if err != nil {
		t.Fatal(err)
	}

	s := grpc.NewServer()

	hellopb.RegisterGreetingServiceServer(s, service.NewHelloService())

	reflection.Register(s)

	go func() {
		s.Serve(l)
	}()
	t.Cleanup(func() {
		s.GracefulStop()
	})

	ctx := context.Background()
	opts := []runn.Option{
		runn.T(t),
		runn.Runner("greq", fmt.Sprintf("grpc://%s", addr)),
	}

	o, err := runn.Load("runbooks/*.yml", opts...)
	if err != nil {
		t.Fatal(err)
	}

	if err := o.RunN(ctx); err != nil {
		t.Fatal(err)
	}
}
ぱんだぱんだ

複数ステップの実行

desc: ログインしてHello Worldする
runners:
  #  gRPCランナーを指定
  greq: grpc://localhost:8080
vars:
#  username: ${USERNAME}
#  password: ${PASSWORD}
  username: yamanaka
  password: pass
steps:
  -
    desc: Step.0 ログインする
    greq:
      hello.GreetingService/Login:
        message:
          name: "{{ vars.username }}"
          pass: "{{ vars.password }}"
    test: |
      current.res.headers['content-type'][0] == "application/grpc"
      && compare(current.res.message, {"id":"U_0001", "name":"yamanaka"})
      && current.res.status == 0
  -
    desc: Step.1 ログインしたユーザー名でハローする
    greq:
      hello.GreetingService/Hello:
        message:
          name: "{{ previous.res.message.name }}"
    test: |
      current.res.headers['content-type'][0] == "application/grpc"
      && compare(current.res.message, {"message":"Hello, yamanaka!"})
      && current.res.status == 0

varsの変数は環境変数か?

ぱんだぱんだ

CLIでシナリオ実行する場合、DBはテスト用で用意しないといけないのか。

実際にシナリオテスト導入するときの動かし方について

  1. CLIでシナリオ実行 + テスト用の環境必要(アプリとDB)
  2. テストコードでシナリオ実行 + テスト用の環境必要(DBのみ)
  3. テストコードでシナリオ実行 + コンテナ(CI実行環境でDBコンテナ立てる) + テストデータはコンテナ起動時に投入するとか -> 遅そう
  4. テストコードでシナリオ実行 + コンテナ(CI実行環境でDBコンテナ立てる) + テストデータは都度作成&削除して真っ新な状態でテストを実行することに努める -> テストデータ作るのが大変そう、てか大変だと思う

単体テストや結合テストはちゃんと書けている前提なら3, 4までやるとテストコードのコード量爆発しそうだし大変だと思うので1か2でやれると楽なんじゃないだろうか

QAチームのようにシナリオテストを作成・実行するチームと開発チームが分かれているならなおさら1, 2の方がやりやすそう。一回テストデータ作ったら使い回しもできて楽そうだし。

デメリットとしてテストデータを都度作成せずに更新していくような運用だと、予期せぬデータ更新でテスト壊れるとかがあるはず。まあ、そしたらなおせばいいのだけど

完全にまっさらでやりたい派は3とか4の方がいいのかな。個人的にはDBまっさらで毎回テスト実行したいけどシナリオテストとかになると大変そうすぎてできるなら1,2がいいなと思った。

ぱんだぱんだ

CLIでもテストコードでやるにしてもどっちにしろ使い心地は申し分なくてもしシナリオテストやろうみたいになったら採用したい

このスクラップは2023/04/07にクローズされました