C#でテストを書いていて、不安定だがリトライすれば通るテスト(Flasky Test、フレークテスト)を避けられないことがあります。XUnitを拡張してRetryFact1を用意するのもいいのですが、もっとさっくりdotnet testをリトライしたいと考えたことはありませんか?私はいつも思ってます。
そこで今回はdotnet testの失敗時にdotnet test
自体をリトライする方法を検証したメモです。
考えられる方法
.NETにおけるテスト実行はdotnet test
コマンドを使いますが、dotnet test
は失敗したテストを再実行する機能がありません。そもそもdotnet testがリトライを持っていれば一番いいですがIssueが特にないんですよね。
dotnet testのテスト失敗時に、dotnet test自体を自動リトライする方法はいくつか考えられます。
- dotnet testを自力でリトライする
- dotnet-test-rerunを使う
- dotnet-retestを使う
簡単にまとめておきましょう。
ツール | 失敗したテストだけリトライ | 日本語出力 | ロガー指定 | GitHubActions連携 | 1000以上のテストを含むプロジェクト |
---|---|---|---|---|---|
dotnet testを自力でリトライする | × (複雑な処理が必要) |
〇 | 〇 | △ (GitHubActionsLoggerと組み合わせでOK) |
〇 |
dotnet-test-rerun | 〇 | △ (英語以外は問題があるかもとのことだが再現せず) |
〇 | △ (GitHubActionsLoggerと組み合わせでOK) |
〇 |
dotnet-retest | 〇 | 〇 | × | 〇 (コードリンクはない) |
△ (エラーになるらしいが再現できず) |
リトライさせるdotnet testを用意する
リトライ検証のため確率で失敗するテストを用意しておきます。
public class StringSplitTest { [Fact] public void SometimesFail() { Random.Shared.Next(0, 3).Should().Be(1); } // ほかにもいろいろテスト }
普通にdotnet test
を実行すると時々成功、時々エラーになります。いわゆるFlaskyテストなのですが、dotnet test
自体は失敗したテストのリトライがないのでCIが不安定になって困るというのが本記事の動機です。
$ dotnet test
実行結果
今回のゴールは、次のようなdotnet test
をリトライできるようにしてみましょう。テスト進捗を出すためconsoleをnormalで出力し、テストが無限ループしたときの実行時間を制約するためRunConfiguration.TestSessionTimeout
2を指定するなどしています。
$ dotnet test --logger "console;verbosity=normal" --logger GitHubActions -- RunConfiguration.TestSessionTimeout=1000
実行結果
dotnet testを自力でリトライする
幸いにも、dotnet test
はテストが失敗すると終了コードが0ではなくなるので、これを利用してリトライできます。
Bash
GitHub ActionsのUbuntuランナーで実行することを想定すると、Bashでサクッと書きたいでしょう。
Bashでリトライするメリットは、dotnet test
をそのまま利用しているので--logger
など各種設定が自由なことです。後述するdotnet testのラッパーには--logger
指定できないものがあります。
retry_limit=3 current=0 while ! dotnet test --logger "console;verbosity=normal" --logger GitHubActions -- RunConfiguration.TestSessionTimeout=1000; do # on failure echo -n "Failed. " # on retry limit check if [[ $current -eq $retry_limit ]]; then echo "max retries reached." exit 1 fi current=$((current+1)) # after failure & before retry echo "retrying. (${current}/${retry_limit})" sleep 1 done
PowerShell
PowerShellでも同様にかけます。適当に上相当の処理を書いてもいいですし、Gistで公開されているhlaueriksson/2023-03-26-retry-flaky-tests-with-dotnet-test-and-powershell.mdを使うのもいいでしょう。
dotnet-test-rerun
joaoopereira/dotnet-test-rerunはdotnet testのラッパーとして、テスト失敗時に自動的にリトライする機能を提供します。
メリット
ラッパーらしく、dotnet test
をdotnet test-rerun
にするだけでおおむね同じように動作します。dotnet test-rerun
独自のオプションが次のように用意されていますが--logger
も問題なく使えるのはいい感じです。オプションは次の通りです。
デメリット
dotnet test
のオプションをdotnet test-rerun
から渡さないといけないので、dotnet test-rerun
がカバーできていないdotnet test
のオプションは使えません。
READMEに書かれている通り、dotnet test
の出力が英語以外の場合は正しく動作しない可能性があります。検証ではテスト名が日本語でも問題ありませんでしたが、どこでトラブルのかな?
Please note that this tool is language-dependent. The output of dotnet test may be localized, and if it is not in English, the tool may not function correctly. Currently, only English is supported.
インストール
dotnet toolsとして提供されています。利用するならプロジェクトのdotnet toolとして導入するのがいいでしょう。
dotnet new tool-manifest dotnet tool install dotnet-test-rerun
使い方
dotnet test-rerun
コマンドを使います。デフォルト設定で利用するなら次のようになります。
dotnet test-retrun プロジェクト.csproj
利用例
サクッと利用してみましょう。
無指定
何も指定せず実行してみましょう。失敗したテストが再実行されているのがわかります。スタックトレースも出ていて割と期待通りですね。
$ dotnet test-rerun
実行結果
何も考えず実行するとレポートファイル(trx)が生成されてしまうのが邪魔です。
$ ls *.trx guitarrapc_HOGWARTS_2025-01-30_23_21_02.trx guitarrapc_HOGWARTS_2025-01-30_23_21_03.trx
自動生成されるtrxをテスト後に自動削除
テスト実行後にレポートファイル(trx)を消すなら--deleteReports
を指定します。ゴミファイルとGit差分が邪魔なので常につけたほうがいいですね。
$ dotnet test-rerun --deleteReports
リトライ回数の調整
リトライ回数を調整するには--rerunMaxAttempts
を指定します。次の例では10回リトライします。
$ dotnet test-rerun --deleteReports --rerunMaxAttempts 10
ログレベルの調整
ログレベルをverboseからnormalに変えても、特に変わらないですね。
dotnet test-rerun --deleteReports --loglevel normal
実行結果
ロガーの追加
ロガーを指定する場合、--logger trx
は必ず含める必要があります。忘れるとリトライされずそこで実行が止まってしまいます。
$ dotnet test-rerun --logger console
実行結果
自分の好みのロガーは--logger
を追加で指定しましょう。例えばconsoleロガーをnormalレベルで出力するならdotnet test
同様に--logger "console;verbosity=normal"
と指定します。[^5]
$ dotnet test-rerun --deleteReports --logger trx --logger "console;verbosity=normal"
実行結果
これを利用すれば、GitHubActionsLoggerも併用できます。GitHubActionsLoggerを使うと、テスト結果のサマリをSTEP_SUMMARYに出力したり、C#的なアナライズ結果をPRのFilesにコード注釈してくれます。
$ dotnet test-rerun --deleteReports --logger trx --logger GitHubActions
実行結果
dotnet testのRunConfigurationを指定
dotnet test -- RunConfiguration.TestSessionTimeout=1000
のようにdotnet test
にさらに追加で渡すオプションは、--inlineRunSettings
を使って渡すことができます。10msで終わるように指定すると時間超過してエラーになりますね。
$ dotnet test-rerun --deleteReports --inlineRunSettings RunConfiguration.TestSessionTimeout=10
実行結果
日本語テスト名
[Fact] public void 時々おちるテスト() { Random.Shared.Next(0, 3).Should().Be(1); }
特に問題なく実行できたので、大丈夫そうか試して使えばいいかも?
$ dotnet test-rerun
実行結果
dotnet testを移行できるのか
目指していたdotnet test
コマンドをdotnet test-rerun
にすれば、そのまま置き換えできました。
$ dotnet test-rerun プロジェクト.csproj --deleteReports --logger trx --logger "console;verbosity=normal" --logger GitHubActions --inlineRunSettings RunConfiguration.TestSessionTimeout=1000
実行結果
dotnet-retest
devlooped/dotnet-retestはdotnet test
のラッパーとして、テスト失敗時に自動的にリトライする機能を提供します。
メリット
dotnet test
にそのままdotnet test引数を渡す設計なのでdotnet-test-rerun
よりもオプションの渡し方がシンプルです。出力にemojiを使ったりと見栄えが強化されます。また、GitHub Actionsと連動を想定してSTEP_SUMMARY
を出力する機能が組み込まれています。
オプションは次の通りです。(後述する理由から0.4.0)
デメリット
Windows以外のロガーはtrx限定です。0.4.1から0.6.3はリトライが機能せず、修正された0.6.4が2025/2/18にリリースされました。
テストプロジェクトに1000以上のテストを含まれているとエラーが生じると報告されており、--no-summary
指定でエラーを回避できるようですが、これは手元で再現しませんでした。
dotnet-retestはテスト結果を出力制御しますが、--no-summary
指定時に絵文字表示が壊れたりします。
GitHub ActionsのSTEP_SUMMARY
を出力する機能は、GitHubActionsLoggerと競合しています。GitHubActionsLoggerはSTEP_SUMMARYでサマリテーブル表示+エラーテストのコードリンクを貼るのに対して、dotnet-retest
はSTEP_SUMMARYに個別のテスト結果を出すもののコードリンクがないので機能的には劣化版です。仮にGitHubActionsLoggerを使っていた場合、PRのFilesにコードリンクが貼られなくなるので残念です。
インストール
dotnet toolsとして提供されています。利用するならプロジェクトのdotnet toolとして導入するのがいいでしょう。
dotnet new tool-manifest dotnet tool install dotnet-retest --version 0.6.4
利用例
サクッと利用してみましょう。
無指定
何も指定せず実行してみましょう。失敗したテストが再実行されているのがわかります。その後成功するとスタックトレースなど詳細は表示されません。
$ dotnet retest
実行結果
リトライ上限を超えても失敗すると詳細が表示されます。スタックトレースが出てるのでどのテストが失敗しているかがわかります。
実行結果
リトライ回数の調整
最新の0.6.4でリトライ回数を調整するには--retries
を指定します。が、0.4.1-0.6.3にはVSTestの出力が変わった影響でリトライされないバグがあります。
$ dotnet retest --retries 3
実行結果
ロガーの追加
ロガーを指定する場合、--logger trx
は必ず含める必要があります。Windowsではconsoleなどを指定できますが、Linuxなど非Windows環境ではtrx以外サポートされていません。自分の好みのロガーが利用できないのでGitHubActionsLoggerも併用できません。
$ dotnet retest -- --logger "console;verbosity=normal"
実行結果
dotnet testのRunConfigurationを指定
dotnet test -- RunConfiguration.TestSessionTimeout=1000
のようにdotnet test
にさらに追加で渡すオプションは、素直に渡せます。
$ dotnet retest -- プロジェクト.csproj -- RunConfiguration.TestSessionTimeout=1000
実行結果
GitHub Step Summary
dotnet retest
をGitHub Actionsで実行すると、STEP_SUMMARYに個別のテスト結果が表示されます。失敗したテストのスタックトレースも表示されますが、GitHubActionsLoggerと異なり、失敗したコード行へのリンクが乗らないので注意です。
1000以上のテストを含むプロジェクト
--no-summary
をつけなくても問題なく実行できました。(1200テストで検証済み)
dotnet testを移行できるのか
目指していたdotnet test
コマンドをdotnet retest -- オプションをそのまま
にして、loggerを消せば移行できます。ロガーがtrx以外使えないのはインパクトが大きく、dotnet-retest自身が出力をきれいにしてくれているとはいえ、ちょっと厳しいものがあります。
$ dotnet retest -- --inlineRunSettings RunConfiguration.TestSessionTimeout=1000
まとめ
dotnet test
の自動リトライはどれもあと一歩手間があります。自力でBashやZxで組むか、dotnet-test-rerunを使うのがインパクト小さく利用できそうです。dotnet-retestは検証する限りでは、リトライが最新版で機能していないのは不信感があり悩ましいです。