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ランブックの実行終了時に実行されて欲しいことが多い...(今欲しいやつ)
クリーンアップ処理単位「runbook単位」より「1実行単位」が良い気がしています。
完全にこっちな気がしてきた
実装を進めている
クリーンアップ処理単位「runbook単位」より「1実行単位」が良い気がしています。
上記を実装しました!
docs/designs/defer.md から転載
通常のステップの実行順が次のような感じ。
# main.yml
steps:
- desc: step 1
test: true
- desc: step 2
test: true
- desc: step 3
test: true
- desc: step 4
include:
path: include.yml
- desc: step 5
test: true
# include.yml
steps:
- desc: included step 1
test: true
- desc: included step 2
test: true
- desc: included step 3
test: true
defer:
がマークされたステップが含まれていると次のような感じ。
# main.yml
steps:
- desc: step 1
test: true
- desc: step 2
defer: true
test: true
- desc: step 3
defer: true
test: true
- desc: step 4
include:
path: include.yml
- desc: step 5
test: true
# include.yml
steps:
- desc: included step 1
test: true
- desc: included step 2
defer: true
test: true
- desc: included step 3
test: true
-
defer:
がマークされると絶対に実行されます(途中のステップがエラーになったとしても) - 複数の
defer:
マークされたステップがある場合はLIFOで実行されます(Includeランナーによって読み込まれたステップも同様に1つのグローバルなLIFO列に追加されます)
制御ステップまとめありがとうございます!
まだ実装を見てない中で質問になって恐縮なのですが、挙動について疑問があるのでまとめます。
- deferを利用した場合のpreviousは実行順の一つ前になる?
- 遅延実行された場合のsteps[].resとか初期化される?
- 遅延実行されたステップで参照しているvarsやstepsも遅延評価される?
- loopするstepでdeferを指定した場合にまとめて遅延される?
- deferのステップのtestセクションの結果は runbook 全体の実行結果(成否)には反映される?
deferを利用した場合のpreviousは実行順の一つ前になる?
はい。
steps:
- desc: step 1
test: true
- desc: step 2
defer: true
test: true
- desc: step 3
test: previous.outcome
の場合 previous.outcome
は step 1 の値を取得します
遅延実行された場合のsteps[].resとか初期化される?
いいえ。残ります。
遅延実行されたステップで参照しているvarsやstepsも遅延評価される?
はい。runRunbookという1つの関数内の変数として捉えてもらえれば良いです。各ステップはその関数内のコード。
loopするstepでdeferを指定した場合にまとめて遅延される?
まとめて遅延されます。
deferのステップのtestセクションの結果は runbook 全体の実行結果(成否)には反映される?
反映されます。
stepリストカウントアップ方式は、 defer 追加後も特に挙動は変わらない認識で良いでしょうか?
もしかするとLIFOのアクセス用の変数を用意したくなるかもなーと思いました。
後処理を行う必要があるイベントが複数回発生しても、後処理は1回で済むケースなど。
stepリストカウントアップ方式は、 defer 追加後も特に挙動は変わらない認識で良いでしょうか?
これ、何をもって変わらないと捉えるか難しいんですけど、
steps:
- desc: step 1
test: true
- desc: step 2
defer: true
test: true
- desc: step 3
test: previous.outcome
だとした時、
desc: step2 の結果は steps[1]
に記録されます。
https://github.com/k1LoW/runn/blob/e647346ddc7a214867cc0a4a173bfb25d70cc675/testdata/book/defer.yml#L1-L30 の test: false
以外のテストランナーの expr は全て true になります。
もしかするとLIFOのアクセス用の変数を用意したくなるかもなーと思いました。
ここらへんはGoのユースケースやドッグフーディングを進めつつ、調整していく(ひつようなものを追加したり挙動を検討したり)のがいいかなあと思っています。
一応 THIS IS EXPERIMENT
ということで
(ちなみに defer:
とは別に、 steps[*].force:
steps.<key>.force:
も提案しようと思っています。Goに慣れていない人はこっちのほうが使いやすいかもしれないと思っています。)
steps[*].force:
steps.<key>.force:
も追加しましたー
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 も依存が書けるならありかもです。
あとは「依存グラフ」の先にある機能を想像しながらのコマンドの設計ですかねえ。
コマンド名も悩む
go-graphviz がPure Goになってしまった...機運かもしれない
ランブックから依存の制御をしたい
具体的には
- includeされることを禁止
- needsされることを禁止
を設定したい。
大きな括りで見ると「シナリオの意図」の設定なんだろうなあ、と思います。
こちら想定しているユースケースとしてはどんな感じでしょうか?
- 2回実行されることを防ぎたい?
- セキュリティ的な観点?
具体的なイメージがあるとインターフェースも決めやすいかと考えています。
依存グラフをシンプルにするためですね。
そのランブックが意図せずinclude/needsされてしまうのを防ぐイメージです。
標準入力をシナリオ内で使いたい
runnからの標準出力を jq
などと連携する方法はすでにあるので、
今度はrunnへの標準入力をシナリオ内で使えるようにしたいです。
もともとは標準入力からRunn IDを取得できるようにしていたのですが、あまり使うことがなかったのと、他にもRunn IDの取得方法はあるので仕様を変えたいと考えています。
どうでしょう?
あ、PRにコメントしてもらっても大丈夫です
標準入力からRunn IDを取得できるようにしていた
こちら把握していなかったですw
パイプで処理した結果をゴニョゴニョできるの良さそうですねー 👍
GoGo!
ですよねw > 把握してない