runn開発者会議スレッド
runn開発者会議 in 鴨川
Show note
- ファイル存在チェック
- Makefile
- Property based testing
- fuzzing test
- テストシナリオのカタログ化
- Gherkin記法
- マイクロサービスのAPIテスト
- VCR cassette
- ゴールデンテストの更新サイクル
- テストダブル
- ステップ実行
ファイルの存在有無などで実行するステップを変えたい
現状の機能ではifセクションでステップの実行可否を制御できる
exec runnerで test -f
でファイルの存在チェックして、ifセクションで参照すればできるがステップが分断されるので可読性が良くない。
また現状ではファイル関係のbuilt-in関数はなし
ファイルの存在チェックの機能があると良さそう
想定しているユースケース
- シナリオ実行の事前条件を実現する
OpenAPIのSpec出力とか
認証情報(Tokenを別途取得しておく) - テスト条件をファイル作成
テストのseedデータを事前取得しておく - APIのレスポンスをキャッシュ化して高速化する
マスターデータ等は、ローカルで保持しておくのも良いかもしれない
実装idea
- makeコマンドのbuildの様に依存しているファイルの関係を定義してステップを変える
targetが存在しないもしくはsourceファイルのほうが、targetファイルよりも新しい場合はステップを実行する - Fileランナーを作っても良さそう
ビルトイン関数よりも筋は良さそう
Property based testingをしたい
OpenAPIのSpecからテストケースを自動生成できないか?
→ ライブラリは幾つかありそう
脆弱性診断を目的とするのであればfuzzing test?
Golangで使えそうなライブラリはあるだろうか?
こちらが見つかったが。。
OpenAPIやgRPCやGraphQLとかのスキーマ定義に対して、汎用的にテストデータを作成できるか?
もし実装するとして、どういうI/Fにするか?
leanovate/gopter の方が有名っぽい
既存の(http|grpc|graphql) runnerのオプショナルなパラメータを追加して、特定のステップでプロパティベースドテストを実行できるようにする
APIチェーン的に引き継ぐべきパラメータとAPI仕様書のプロパティベースドで生成したパラメータをいい感じにmergeして実行できると良さそう。
使い勝手を考えると、通常通り指定されたパラメータの内、生成されたパラメータで上書きしないパラメータ名を指定できると良さそう? 🤔
シナリオテスト自体をドキュメント化したい
現状ではlistコマンドでシナリオ一覧までを出力はできるが、シナリオのステップ及びステップ内のテスト項目も含めてドキュメントすることはできない。
ステップの内容についてはverboseオプション時の出力内容でカバー出来るかもしれないが、実行しないと全体のステップを把握することはできない。
yqコマンドで静的にrunbookを解析してドキュメントを生成することは出来るが、runn側のparse機能をベースにドキュメント出力のサポートができると良さそう(runnをパッケージ利用してドキュメントツール自体は別ツールとして提供するか?)
テスト内容についてはkarateの様にGherkin記法みたいに可読性を担保したテスト式を書けると良さそうであるが、Gherkin記法自体は馴染まなさそう。
jestのdescribeやitとかテストの条件を装飾できると良さそう。
ゴールデンテストのファイルとレスポンスを効率よく比較したい
-
想定するユースケース
CSVファイルをレスポンス出力するようなシナリオで、レスポンス内容をファイルとして保存してゴールデンテストとする -
現状のシナリオのフロー
- CSVファイルをダウンロードするAPIを呼び出す
ステータスコードのみチェック - 期待値となるファイル名と同じパスに拡張子.actualのファイルとしてdumpする
期待値ファイル: path/to/expected.csv
検証対象ファイル: path/to/expected.csv.actual - それぞれのファイルを外部コマンドのdiffで比較する
- diff実行のステータスが0以外(=差分がある)の場合は内容を出力し、testで失敗もさせる
※このステップで failセクション が欲しかった
- CSVファイルをダウンロードするAPIを呼び出す
上記の2〜4までのステップが共通っぽいので、共通のシナリオとしてincludeして実行させている
include時のvarsとして、期待値ファイルのパスと、レスポンスの内容を渡す
- ゴールデンテストの入れ替え作業
テスト結果が失敗したら、actualファイルを手動でcpして期待値ファイルを上書きしている
やりたいこと(解決したいこと)
- ファイルの比較をdiffコマンドをやめたい
オンメモリで比較したい。パフォーマンスの問題と地味にscopeの権限付与が面倒 - ゴールデンテストとなる期待値のファイルの差し替えを自動化したい
イメージとしてはUPDATE_GOLDEN環境変数みたいにオプション指定したら期待値のファイルを書き換えたい
vcrっぽい感じですね。
全く同じようなものにはなりませんが、
https://pkg.go.dev/github.com/k1LoW/runn#Capture に https://pkg.go.dev/github.com/k1LoW/runn/capture#Runbook を指定できるように runn run
のオプションを拡張すると runn らしさがでるかもしれないと思いました。
2024年中にv1にしたい
もうマイナーバージョンが3桁になる状況なので2024年中にv1にしたいです。
が、何をトリガーにv1にするべきか...ということに悩んでいます
永遠のベータは最近は流行らないですかねー? 🤔
注目度が上がり、他のツール等の連携も増えてきているので、ちゃんとメジャーリリースした方が対外面では良さそうという判断でしょうか?
一つ確認しておきたいのですが、バージョンのナンバリングには意味を持たせますか(持たせたいですか)?
v1はLTSバージョンでv2は開発バージョン(比較的I/F変更が活発)とか。。
tagpr使っているので、2つのメインストリームを持たせるのは難しいかもと思っていますが。
考えられるトリガーは以下の感じですかね。
- Breaking Changeがある機能が反映されてから(反映してから)
- 初期構想の機能がおおよそ実装が出来た場合
- 新しい機能追加の方向性が決まった段階
- 周年(ファーストコミットからの経過日数?)
- 依存しているライブラリがメジャーバージョンアップした場合(Golangのバージョンも?)
注目度が上がり、他のツール等の連携も増えてきているので、ちゃんとメジャーリリースした方が対外面では良さそうという判断でしょうか?
はい。主に「オペレーションツールもしくはテスティングツールとして機能を枯らしたい」というのが理由です。
いつまでたってもBreaking Changeがガンガンあるようなツールだと使う側(runnの上に資産を積み上げる側)としては安心できないと思っています。
v1にしたあかつきには機能の追加はあっても挙動の変更はしないようにしたいと考えています。
バージョンのナンバリングには意味を持たせますか(持たせたいですか)?
はい。なの「でできるかぎりSemVer」を採用したいと思っています。
幸い、Goは後方互換性を保ちつつバージョンアップをしています。Goがv1の間は何も考える必要はないと思っています。runnもそのようにしたいと考えています。
とはいえ開発体制から考えてもLTSなどはサポートできないので、単純にSemVerで開発すればいいのではないかと思っています。
tagpr使っているので、2つのメインストリームを持たせるのは難しいかもと思っていますが。
これは必要になったらtagprに機能追加を提案すればいけそうだと考えています。
考えられるトリガーは以下の感じですかね。
良いと考えているのは「初期構想の機能がおおよそ実装が出来た場合」なんですけど、すでに私の想定を超えているので、どうしたもんかなーと。
まあ、「SemVerにする(v1にして挙動の変更はバージョンを上げる)」というだけなので記念的な何かでもいいとは思っています。
ちなみに、ガンガン開発した結果、v1がv100に到達してもそれは別に問題ないと思っています。なので開発に影響はないかなと...(たぶん気分の問題になりそう)
runn lint
ある程度「こうあった方がいい」というようなルールを提供したいなと思っています。
- ランブックのdescがない
- ステップのdescがない
- ランブックのdescが重複している
- ランブックの書き方が List と Map で混在している
などがあるだけで便利かなあと。
同一step内で結果確認する場合はcurrentを使っているか?とかですかねー
それも良さそうですね!
includeでのファイル名の大文字・小文字違いで環境依存でテスト失敗するケースがあるのでlintで気づきたいです。
includeでのファイル名の大文字・小文字違いで環境依存で
お、これはどう判定する(何を正しいとする)といいんですかねー
ランブック単位でのクリーンアップ処理
afterHookとかtearDownとかCleanupとか言われるアノ機能です。
まず、個人的には(beforeなんとか)前処理はいらないと思っています。steps:
に書けば良い。
その上でクリーンアップ処理は後処理というか後始末処理としてあるのは良さそうな気はしています。
runnをGoのパッケージとして使う場合の実装はすでに runn.BeforeFunc と runn.AfterFunc があるので、ランブックで実現するクリーンアップ処理について考えたいです。
関連Issue
今のところ良いと思っているアイデアは「ステップへの defer:
の導入です」
Go言語における defer ステートメントとほぼ同じ挙動をするイメージです。
メリット
- ステップの実行の途中で順々に後処理を追加できるという柔軟性がある
- ステップをそのまま後始末処理に回せるのでランブックのYAMLのネストが増えないし新しいセクションも増えない
デメリット
- defer の考え方が Gopher 以外にはわかりにくい(
defer:
でなくてもいいのかも) -
step[*]
構文やcurrent
previous
構文と相性が悪い- しっかり検討してデザインをする必要がある
ステップへの defer: の導入です
runBookに対してdeferなのか?stepに対してdeferなのかによって結構I/Fが変わりそう
runBookだったら、後処理のイメージがつきやすい?
step[*] 構文や current previous 構文と相性が悪い
last
という構文を用意して、どのstepまで処理されたのか?確認しながら
ステップの実行の途中で順々に後処理を追加できる
runBookでもincludeさせればそれほど制約にはならなさそう。
runBookに対してdeferなのか?stepに対してdeferなのかによって結構I/Fが変わりそう
ランブックに対してです。
ランブックをGoにおける関数ブロック、各ステップを関数ブロック内の処理だとイメージしてもらえると良いかと。
last という構文を用意して、どのstepまで処理されたのか?確認しながら
どちらかというと defer とマークされたstepは実行されずにランブックの終了まで待つイメージです。
次のようなランブックがあったとき
steps:
-
test: true
-
defer: true
test: true
-
test: false
実行順は0->2->1になります。
かつ、2が失敗しても1はdeferとマークされているので実行されます。
そうすると、「step[*] 構文や current previous 構文と相性が悪い」というのがわかるかと思います。
具体的には3つめのstep(つまり2)で previous
を使っていた場合、それは0にかかるのか1にかかってしまうのか。みたいな感じで悩ましいのです。
ステップの実行の途中で順々に後処理を追加できる
runBookでもincludeさせればそれほど制約にはならなさそう。
steps:
-
test: true
-
defer: true
test: true
-
test: false
-
defer: true
test: true
-
test: true
このとき、実行順は0->2->1です。
もし2が test: true
なら 0->2->4->3->1 です。
このように段階的に後処理(1と3)を追加できるのが便利です。
goconでディスカッションした内容をベースに決めたいことをまとめました
論点 | 案1 | 案2 | 案3 |
---|---|---|---|
クリーンアップ処理単位 | runBook | step単位 | runner単位 |
stepリストカウントアップ方式 | 枝番あり | 実行順番で単純カウントアップ。枝番なし | カウントアップなし |
前処理結果判定方法 | 専用別名(last) | 既存方式(previous) | deferred-objectベース done, fail, always |
実行タイミング | runBook終了後 | step終了後 | - |
同期 | クリーンアップ処理終了までwait | 非同期 | - |
クリーンアップ処理中のエラーハンドリング | なし 前処理の結果次第で停止と継続 |
あり testセクションサポートあり test結果次第で停止 |
非同期でハンドリングNG |
クリーンアップ処理の後方参照 | 枝番で参照可能 | クリーンアップ処理は通常のstepと同様に結果保持 | 非同期で結果保持なし |
どの論点を軸に優先させたいか?によって案の組み合わせはいくつかのパターンで表せるハズ。
パターンをABCでまとめて判断してみる。
私個人としては「runbook単位」のみかなあと思っています。
理由は「外部APIの実行に失敗したらDBの処理を戻す」「アップロードに失敗したら一時ファイルを削除する」など、後始末処理が複雑なユースケースがあるため、ステップ単位やランナー単位では対応できないと考えているためです。
クリーンアップ処理単位「runbook単位」より「1実行単位」が良い気がしています。
Include先で書いた defer はそのIncludeした側のrootランブックの実行終了時に実行されて欲しいことが多い...(今欲しいやつ)
JSON Schemaの導入
具体的に何に使いたいかというと、各ステップのフォーマットベースのバリデーションです。
書きやすさを重視していることからネストが1つずれるだけで意図した形で動かなくなります。
なのでJSON Schemaによるバリデーションを内蔵したいなあと考えています。各ランナーごとに。
その先には、カスタムランナーのカスタムバリデーションをJSON Schemaで追加できるようになると良いなあと思っています。
各ランナーのValidatorというとHttpRunnerのOpenAPI Specと、gRPCRunnerのprotoがありますが、それとは別ということでしょうか?
runBookの構文チェック的にJSON Schemaを定義する感じでしょうか?
それとも、どこのyamlの一部分に対してJSON Schameを使って構文をチェック感じでしょうか?
構文のほうです!
CEL(Common Expression Language) 対応
個人的には、expr-langで全く困ってないのですが、新規ユーザはCELのほうがいいのかも?などと思っています。
できれば「両方対応」が実現できたらいいなあと思っています。
個人的には、expr-langで全く困ってないのですが
同じく。
filter関係も結構使ってますが、こういうのもCommon Expression Languageであったりするんですかね?
全く同じ機能というのはないと思います。
https://codelabs.developers.google.com/codelabs/cel-go#7 な感じで拡張が必要そうです。
ステップの時間計測
ステップ毎にかかった時間を計測したい
定点的に観測して遅くなったステップの変化を捉えたい
オプションをつけたらかかったステップの時間が表示されるでも良いかも知れない
ステップ毎にかかった時間を計測したい
profileで取得できますね。逆にいうと実行時間はprofileをonにするのと同じことになります。
needs:
セクションの追加
依存するランブックを指定でき、そのランブックの実行結果(bindした値)を使用したランブックが書けるようにする。
Includeランナーと違うのは依存先ランブックに対して何も干渉ができない代わりに、
ランブックAの値を、(2度とランブックAを実行せずに)ランブックBとCで再利用して使うということが可能になる。
[...]
needs:
init: path/to/init.yml
apiinit: path/to/apiinit.yml
steps:
-
exec:
command: echo '{{ needs.init.secret }}'
needsセクションを作ることができたら、いわゆる前処理の書き方に選択肢が生まれる
凄い未来を感じます。
妄想ですが、複数シナリオ実行時に
- needsで指定されたシナリオが既に実行済みの場合はその結果を踏まえて処理する
- needsで指定されたシナリオが未実行の場合は、ランブック実行前にシナリオ実行される
となったら、処理時間の短縮にも繋がりそう。
bind変数を保持し続けないといけないのでメモリが凄いことになりそうですが 😅
妄想ですが、複数シナリオ実行時に
求める挙動が完全に同じです!
処理時間の短縮にも繋がりそう。
まさにこれがしたい感じですね。
bind変数を保持し続けないといけないので
現状、実行後にstoreの値は消していないんですよね。
https://github.com/k1LoW/runn/blob/b607f602729f286fd8f747c4bb76fd2e790c2f49/operator.go#L115-L118 で実行後に値を取れたりするので。
なのでメモリ使用量は変わらない可能性があります(調整もしないとな)。
無事実装しました
APIシナリオテストをMockサーバー(テストダブル)化
APIシナリオテストからシナリオ内容からMockサーバー化できないか?という妄想。
単純にMockサーバーにするという話ではなくテストダブルにできないか?という内容です。
ゴールのイメージとしては以下の様になります。
SPAなシステムのバックエンドのAPIのテストをrunnで書いたら、フロントエンドのテスト用にモックサーバーを立ち上げることができ、フロントからシナリオどおりのレスポンスを返却することができる。
シナリオテストの想定以外のリクエストが送信された場合に、エラーにするという内容です。
これはマイクロサービス間のAPIテストで環境を整備するのが辛い問題を解決する一つのアイデアになります。
鴨川でお話をしたテストダブルの件と同じでしょうか?
以下の記事が参考になると思います。Karate vs Karateのイメージが素敵です。
なるほど!すっきり理解できたかもです。
frontend -> backend な構成において
- runn -> backend でシナリオテストを実行する
- この時runnはリクエストとレスポンスを何かしらの形でファイル等に書き出しておく(仮に「カセット」とする)
- runn(もしくは別のなにか)はカセットを使ってモックサーバとして起動する
- frontend -> runnモックサーバでテストが実行できる
こんなイメージでしょうか?
ですですー。
カセットという用語は良さそうですね。
VCRを連想させますね。
VCRのカセットと同様の挙動でいいのか?シナリオならではの+αがあるのか?は議論はほしそうですねー
カセットがAPIとの通信のキャプチャ&リプレイさせる為だけの情報にするか?
カセットの情報にシナリオの可変値を識別して埋め込んでおくか?
varsを可変値として柔軟なMockサーバーになりそう(実装は大変そうですが 😅
個人的にはrunnとモックサーバとの間に共通のフォーマット(カセット的なもの)を作る(もしくは既存のものがあればそれに乗っかる)のが良いかなあと思います。
その共通フォーマットにプログラマブルな要素を含めるのか、それとも、そのカセットをいい感じにいじる機能をモックサーバ側に作る かなあと思っています。
なので
カセットの情報にシナリオの可変値を識別して埋め込んでおくか?
上記ではない感じです。runnのランブックとは切り離したフォーマットで良いかなと(当然近いフォーマットでもいいわけですが)。
まあ実現できそうなのはHTTPランナーとgRPCランナーのシナリオだけですね。
複数のランブックの依存グラフの出力
Includeランナーや needs:
セクションの導入の結果、ランブックの依存関係の把握はシナリオの整理の上で必要になってきたかもしれない。
依存グラフを何かしらの形で出力できれば良さそう。
(なお、今回は go-graphviz の導入には慎重です。Cgoが必要になってしまうため)
今ならGithub ReadyなMermaid形式がいいんですかね?
tblsでERD出す時に使っていませんでしたっけ?
Mermaidもいいと思いますー DOTまでならテキスト出力ですし PlantUML も依存が書けるならありかもです。
あとは「依存グラフ」の先にある機能を想像しながらのコマンドの設計ですかねえ。
コマンド名も悩む
ランブックから依存の制御をしたい
具体的には
- includeされることを禁止
- needsされることを禁止
を設定したい。
大きな括りで見ると「シナリオの意図」の設定なんだろうなあ、と思います。
こちら想定しているユースケースとしてはどんな感じでしょうか?
- 2回実行されることを防ぎたい?
- セキュリティ的な観点?
具体的なイメージがあるとインターフェースも決めやすいかと考えています。
依存グラフをシンプルにするためですね。
そのランブックが意図せずinclude/needsされてしまうのを防ぐイメージです。
runn.Capturer インターフェースのあるべき姿を考える
- 契機
- 議題
execランナーで重たい処理を実行する際に、現状のCapturerインターフェースでは処理が完了するまでデバック実行がされない。
Capturerインターフェースのあるべき姿を考えたい
runn.Capturerインターフェースの現状整理
各種runnerが実行時の状態をキャプチャーする為のインターフェースが定義されている
このインターフェースを通して
これらのキャプチャーする機能を実装して以下の機能を実現している
- ステップ毎の実行結果やエラーのキャプチャしてstoreへの格納や、verbose出力
- 各種runnerの実行結果をキャプチャーしてデバック出力
現状のインターフェースの課題
- ログやデバック観点でキャプチャしたいイベントと、最終結果を格納したいタイミングが異なる
※今回はデバック出力文脈でイベントを増やそうとしている
デバック文脈では、最終結果がFix。。ステップが完了する前の状態を扱ったりする - runnerが増えて新しいキャプチャ内容が増えると、拡張ポイントが増えすぎる?
確定結果を扱うインターフェースと、途中結果を扱うインターフェースが混ざっているかもしれない。(今回のexecの標準出力は1行毎にインベント通知したさがあるが、現状では全ての出力結果を一つの文字列として扱っている) - インターフェース定義されている関数によっては、例えばstoreの格納文脈等では実装しづらいものがある
Execのログに関していうと
https://github.com/k1LoW/runn/issues/986 とも関係するかなあと思っています
CaptureExec*
だけストリーム(io.Writer)が欲しくなっていませんか?(私はそうです)
いただいだ「現状のインターフェースの課題」をみると、もう少し広く考える必要がありそうな気もしますね。。。
CaptureExec* だけストリーム(io.Writer)が欲しくなっていませんか?(私はそうです)
CaptureSSH*
もですねー
runner側はStdoutPipeとかでストリーム処理する感じになってしますしね。
ストレームベースにするか?テキストベースで1行づつ処理する感じにするか?どうかはまだ悩んでいます。
storeにstdoutやstderrを格納する際に、既存のCaptureExecStdoutとCaptureExecStderrを残すのか?
それとも既存のインターフェースを廃止して新しくストリーム処理させる function 側でstoreに格納させるのが良いか?が関わってくると考えています。
それによってテキストベースで処理させるインターフェースの方が望ましいのか?が決まってくると考えています。
storeにstdoutやstderrを格納する際に、既存のCaptureExecStdoutとCaptureExecStderrを残すのか?
なるほど...うおおどうすればいいんだ....
storeにstdoutやstderrを格納する際に、既存のCaptureExecStdoutとCaptureExecStderrを残すのか?
一旦、残した状態で実装を進めてみました。
もう一点課題が出てきました。
標準出力と標準エラー出力のハンドリングを独立して非同期化するか?という課題が発生しました。
出力内容が混ざってしまう(それに伴うテストがFlaky化する)問題が出てきました。
これはdebug時を考慮して標準出力→標準エラー出力で逐次処理の方が、良さそうでしょうか?
正常な出力と考えると混ざるべきだと思っています。
うーんまだ行ごとに出力するようなメソッドを作成する方針に振れない自分がいます。
なるほどー
混ぜるとテストしづらさが出てしまうので悩ましいですね
うーんまだ行ごとに出力するようなメソッドを作成する方針に振れない自分がいます。
インターフェースを変更せずにexec runnerにTeeオプションを追加して
オプションが有効になっている場合にのみTeeReaderを挟む様にして標準出力するようにするにはどうでしょうか?
debugとの責務とは独立したオプションというイメージです。
exec runnerのみを拡張するというのはミニマムで良いかもですね(オプション名、 tee 以外にいい感じのないですかねー)。これ賛成です。
とはいえ、debugよりもexec runnerのオプションのほうが情報量が多いのはアレですね。
なんとかしたい...ウルトラCがあればいいんですけど。
混ぜるとテストしづらさ
「STDINとSTDOUTの情報量全てが出力されているか」とかで良さそうに思いました。本来コマンドの出力をstreamにしている状態ではSTDINとSTDOUTが混ざっていますし。
今回実現したい機能の特性を考えるとそのほうが自然に思いました。
tee(?)オプションはあくまで「ストリームで出力を見たい」だけという立ち位置で良さそうです。
オプション名、 tee 以外にいい感じのないですかねー
liveoutput ではどうでしょうか?
tee(?)オプションはあくまで「ストリームで出力を見たい」だけという立ち位置で良さそうです。
大分シンプルになりました:+1:
最高です!!!!!!!!!!!!!名前もめちゃいい!!!!!!個人的に気に入りました。
1点だけ、
英語+YAML的には liveOutput
になると思うのですがどうしましょう?
Merged