tech.guitarrapc.cóm

Technical updates

dotnet testのテスト失敗時に自動リトライさせる

C#でテストを書いていて、不安定だがリトライすれば通るテスト(Flasky Test、フレークテスト)を避けられないことがあります。XUnitを拡張してRetryFact1を用意するのもいいのですが、もっとさっくりdotnet testをリトライしたいと考えたことはありませんか?私はいつも思ってます。

そこで今回はdotnet testの失敗時にdotnet test自体をリトライする方法を検証したメモです。

考えられる方法

.NETにおけるテスト実行はdotnet testコマンドを使いますが、dotnet testは失敗したテストを再実行する機能がありません。そもそもdotnet testがリトライを持っていれば一番いいですがIssueが特にないんですよね。

dotnet testのテスト失敗時に、dotnet test自体を自動リトライする方法はいくつか考えられます。

  1. dotnet testを自力でリトライする
  2. dotnet-test-rerunを使う
  3. 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

実行結果

Image

今回のゴールは、次のようなdotnet testをリトライできるようにしてみましょう。テスト進捗を出すためconsoleをnormalで出力し、テストが無限ループしたときの実行時間を制約するためRunConfiguration.TestSessionTimeout2を指定するなどしています。

$ dotnet test --logger "console;verbosity=normal" --logger GitHubActions -- RunConfiguration.TestSessionTimeout=1000

実行結果

Image

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 testdotnet test-rerunにするだけでおおむね同じように動作します。dotnet test-rerun独自のオプションが次のように用意されていますが--loggerも問題なく使えるのはいい感じです。オプションは次の通りです。

image

デメリット

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

実行結果

image

何も考えず実行するとレポートファイル(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

実行結果

image

ロガーの追加

ロガーを指定する場合、--logger trxは必ず含める必要があります。忘れるとリトライされずそこで実行が止まってしまいます。

$ dotnet test-rerun --logger console

実行結果

image

自分の好みのロガーは--loggerを追加で指定しましょう。例えばconsoleロガーをnormalレベルで出力するならdotnet test同様に--logger "console;verbosity=normal"と指定します。[^5]

$ dotnet test-rerun --deleteReports --logger trx --logger "console;verbosity=normal"

実行結果

image

これを利用すれば、GitHubActionsLoggerも併用できます。GitHubActionsLoggerを使うと、テスト結果のサマリをSTEP_SUMMARYに出力したり、C#的なアナライズ結果をPRのFilesにコード注釈してくれます。

$ dotnet test-rerun --deleteReports --logger trx --logger GitHubActions

実行結果

image

dotnet testのRunConfigurationを指定

dotnet test -- RunConfiguration.TestSessionTimeout=1000のようにdotnet testにさらに追加で渡すオプションは、--inlineRunSettingsを使って渡すことができます。10msで終わるように指定すると時間超過してエラーになりますね。

$ dotnet test-rerun --deleteReports --inlineRunSettings RunConfiguration.TestSessionTimeout=10

実行結果

image

日本語テスト名

[Fact]
public void 時々おちるテスト()
{
    Random.Shared.Next(0, 3).Should().Be(1);
}

特に問題なく実行できたので、大丈夫そうか試して使えばいいかも?

$ dotnet test-rerun

実行結果

image

dotnet testを移行できるのか

目指していたdotnet testコマンドをdotnet test-rerunにすれば、そのまま置き換えできました。

$ dotnet test-rerun プロジェクト.csproj --deleteReports --logger trx --logger "console;verbosity=normal" --logger GitHubActions --inlineRunSettings RunConfiguration.TestSessionTimeout=1000

実行結果

image

dotnet-retest

devlooped/dotnet-retestdotnet testのラッパーとして、テスト失敗時に自動的にリトライする機能を提供します。

メリット

dotnet testにそのままdotnet test引数を渡す設計なのでdotnet-test-rerunよりもオプションの渡し方がシンプルです。出力にemojiを使ったりと見栄えが強化されます。また、GitHub Actionsと連動を想定してSTEP_SUMMARYを出力する機能が組み込まれています。

オプションは次の通りです。(後述する理由から0.4.0)

image

デメリット

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

実行結果

image

リトライ上限を超えても失敗すると詳細が表示されます。スタックトレースが出てるのでどのテストが失敗しているかがわかります。

実行結果

image

リトライ回数の調整

最新の0.6.4でリトライ回数を調整するには--retriesを指定します。が、0.4.1-0.6.3にはVSTestの出力が変わった影響でリトライされないバグがあります。

$ dotnet retest --retries 3

実行結果

image

ロガーの追加

ロガーを指定する場合、--logger trxは必ず含める必要があります。Windowsではconsoleなどを指定できますが、Linuxなど非Windows環境ではtrx以外サポートされていません。自分の好みのロガーが利用できないのでGitHubActionsLoggerも併用できません。

$ dotnet retest -- --logger "console;verbosity=normal"

実行結果

image

dotnet testのRunConfigurationを指定

dotnet test -- RunConfiguration.TestSessionTimeout=1000のようにdotnet testにさらに追加で渡すオプションは、素直に渡せます。

$ dotnet retest -- プロジェクト.csproj -- RunConfiguration.TestSessionTimeout=1000

実行結果

image

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は検証する限りでは、リトライが最新版で機能していないのは不信感があり悩ましいです。


  1. xUnit.netのv3版RetryFactはこちら
  2. RunConfiguration.TestSessionTimeout=1000はdotnet testに1000ms(1秒)タイムアウトを設定する