Start Debugging

修正: Microsoft.Testing.Platform で dotnet test が終了コード 5 と "Zero tests ran" を返す

終了コード 5 は、テストがないのではなく MTP がコマンドラインオプションを拒否したことを意味します。テスト実行ファイルを直接実行して本当のエラーを確認し、拡張パッケージを追加するか、オプションをプロジェクトごとに振り分けてください。

dotnet test が Zero tests ran に続けて Exit code: 5 を出力する場合、渡した引数のどれかをテストアプリが拒否したため、テストは一度も検出されていません。Microsoft.Testing.Platform (MTP) では、終了コード 5 は「コマンドライン引数が無効」を意味しますが、dotnet test はその説明を捨ててしまいます。同じ引数でビルド済みのテスト実行ファイルを直接実行して (./bin/Debug/net10.0/MyTests --logger trx)、本当のメッセージを確認してください。そのうえで、そのオプションを提供する拡張パッケージを参照するか、VSTest 時代のフラグを MTP の同等フラグに置き換えるか、TestingPlatformCommandLineArguments でそのオプションを理解するプロジェクトにだけ振り分けます。

以下はすべて、macOS arm64 上の .NET 10 SDK 10.0.302 と .NET 11 SDK RC 1 (11.0.100-rc.1.26425.128) で、MSTest.Sdk 4.4.1 (Microsoft.Testing.Platform 2.4.1)、MSTest.Sdk 4.3.3 (MTP 2.3.3)、xunit.v3.mtp-v2 4.0.1 を使い、dotnet test を MTP モードに切り替える global.json を置いて計測した結果です。

エラーの全体像

これは、問題のない 2 つのテストを持つ MSTest プロジェクトに対して、SDK 10.0.302 で dotnet test --project MsTests --logger trx を実行したときの出力のすべてです。

MsTests/bin/Debug/net10.0/MsTests.dll (net10.0) Zero tests ran
Exit code: 5

Test run summary: Zero tests ran
  error: 1

  total: 0
  failed: 0
  succeeded: 0
  skipped: 0
  duration: 82ms
Test run completed with non-success exit code: 5 (see: https://aka.ms/testingplatform/exitcodes)

足りないものに注目してください。ターゲットフレームワークが (net10.0|arm64) ではなく (net10.0) と表示されており、Running tests from ... の行もありません。テストホストはテスト検出に到達していません。--output Detailed、-v detailed、--diagnostic を追加しても理由は表示されず、--diagnostic が書き出す .diag ファイルにも生のコマンドラインが記録されるだけです。

.NET 11 RC 1 SDK では、サマリー行が Test run summary: Failed! に変わり、新しい Handshake failures: セクションにモジュールが列挙されますが、理由は依然として表示されません。

複数のテストプロジェクトがあるソリューションでは、1 つが失敗して別の 1 つが成功するため、さらに読み違えやすくなります。

XTests/bin/Debug/net10.0/XTests.dll (net10.0) Zero tests ran
Exit code: 5
MsTests/bin/Debug/net10.0/MsTests.dll (net10.0|arm64) passed (848ms)
Test run summary: Failed!
  error: 1
  total: 2

MTP が終了コード 5 を返し、テストが 0 件と表示される理由

MTP のテストプロジェクトは通常のコンソール実行ファイルです。MTP モードの dotnet test は各テストプロジェクトをビルドし、実行ファイルを起動し、自身が消費しない引数をすべて転送して、名前付きパイプ経由でプロセスと通信します。テストアプリは何よりも先にコマンドラインを検証します。未知のオプションがある場合や、既知のオプションに無効な値が渡された場合は、終了コード 5 で終了し、テストを 1 件も検出しません。MTP の終了コード表は、5 を「テストアプリに渡されたコマンドライン引数が無効」と定義しています。

すると dotnet test は、結果を出さなかった他のモジュールと同じ方法でそのモジュールを報告します。それが Zero tests ran です。この文言は技術的には正しいものの、まったく誤解を招きます。[TestMethod] 属性の欠落やフィルターの不具合を探しに行かせてしまうからです。

オプションが未知になる原因は 4 つあり、実際のパイプラインで見かける順に並べています。

  1. VSTest のフラグが移行後も残っている。 --logger trx、--collect "XPlat Code Coverage"、--blame-hang-timeout 5m、--blame-crash は VSTest のものです。MTP には --logger も --collect もありません。
  2. 拡張パッケージがない。 MTP のコアにはレポーター、カバレッジ、ダンプ、リトライが含まれません。--report-trx は Microsoft.Testing.Extensions.TrxReport を参照したときにだけ存在し、--coverage には Microsoft.Testing.Extensions.CodeCoverage が必要で、他も同様です。
  3. 1 つのソリューションにフレームワークが混在している。 MSTest.Sdk は TRX とコードカバレッジをデフォルトで有効にしますが、素の xUnit v3 プロジェクトは有効にしません。同じ dotnet test --solution のコマンドラインが、一方のプロジェクトでは有効で、もう一方では無効になります。
  4. 有効なオプションに不正な値を渡している。 エージェント上に存在しないファイルを指す --settings や、単位の接尾辞がない --timeout 30 などです。

終了コード 8 は、同じ Zero tests ran の文言を出す別の失敗です。この場合は引数に問題がなく、テストアプリは検出を実行しましたが、何も一致しませんでした。Exit code: の行と、Running tests from ... が表示されているかどうかで見分けられます。

最小再現

global.json で dotnet test を MTP モードにオプトインします (.NET 10 SDK 以降で必要です。これがないと VSTest ブリッジのままです)。

// .NET 10 SDK 10.0.302
{
  "sdk": { "version": "10.0.302" },
  "test": { "runner": "Microsoft.Testing.Platform" }
}

MSTest SDK を使う MSTest プロジェクトです。

<!-- MsTests/MsTests.csproj, MSTest.Sdk 4.4.1 (Microsoft.Testing.Platform 2.4.1) -->
<Project Sdk="MSTest.Sdk/4.4.1">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
</Project>
// MsTests/Tests.cs, .NET 10, MSTest 4.4.1
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MsTests;

[TestClass]
public class CalculatorTests
{
    [TestMethod]
    public void Adds() => Assert.AreEqual(4, 2 + 2);

    [TestMethod, TestCategory("Slow")]
    public void Multiplies() => Assert.AreEqual(6, 2 * 3);
}

そして、その隣にある xUnit v3 プロジェクトです。

<!-- XTests/XTests.csproj, xunit.v3.mtp-v2 4.0.1 -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <OutputType>Exe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="xunit.v3.mtp-v2" Version="4.0.1" />
  </ItemGroup>
</Project>

SDK 10.0.302 で各コマンドが返した結果は次のとおりです。

コマンド終了コードサマリーの内容
dotnet test --project MsTests0成功、2 テスト
dotnet test --project MsTests --logger trx5Zero tests ran
dotnet test --project MsTests --collect "XPlat Code Coverage"5Zero tests ran
dotnet test --project MsTests --blame-hang-timeout 5m5Zero tests ran
dotnet test --project MsTests --report-trx0成功、TRX を出力
dotnet test --solution All.sln --report-trx5MSTest は成功、xUnit は “Zero tests ran”
dotnet test --solution All.sln --coverage5MSTest は成功、xUnit は “Zero tests ran”
dotnet test --solution All.sln --settings x.runsettings (ファイルなし)5両方とも “Zero tests ran”
dotnet test --project MsTests --filter "TestCategory=DoesNotExist"8Zero tests ran (本当に 0 件)
dotnet test --project MsTests --minimum-expected-tests 59期待される最小テスト数のポリシー違反

修正の詳細

1. テストアプリから本当のエラーメッセージを取り出す

CI が渡しているのとまったく同じ引数で、ビルド済みの実行ファイルを自分で実行します。Windows では MsTests.exe で、Linux と macOS では拡張子がありません。

# .NET 10 SDK 10.0.302, MTP 2.4.1
./MsTests/bin/Debug/net10.0/MsTests --logger trx
Unknown option '--logger'
Option '--logger' uses VSTest syntax, which is not supported by Microsoft.Testing.Platform.
Use '--report-trx' instead.
Run '--help' to see the options registered by this test application. If the option belongs to an extension, ensure its package is referenced and the extension is registered.
Command line: --logger trx

dotnet run --project MsTests --no-build -- --logger trx でも同じ内容が表示され、同じく 5 を返します。“uses VSTest syntax … Use ‘—report-trx’ instead” というヒントは MTP 2.4 で追加されました。MTP 2.3.3 (MSTest.Sdk 4.3.3) では Unknown option '--logger' とそれに続くヘルプテキストだけが表示されますが、それでも十分です。

次に、アプリが実際に持っているオプションを問い合わせます。--help はプラットフォームのオプションと、参照しているパッケージが提供する “Extension options” を別々に列挙します。--info はプラットフォームのバージョンと、登録されているすべての拡張をそれぞれのバージョン付きで出力します。

# .NET 10 SDK 10.0.302
./XTests/bin/Debug/net10.0/XTests --help
./XTests/bin/Debug/net10.0/XTests --info

渡しているオプションがその一覧にない場合は、終了コード 5 で失敗します。この確認だけで、この種の問題の大半は解決します。

2. VSTest のフラグを MTP の同等フラグに置き換える

VSTest の引数MTP の引数提供するパッケージ
--logger trx--report-trxMicrosoft.Testing.Extensions.TrxReport
--logger trx;LogFileName=x.trx--report-trx --report-trx-filename x.trxMicrosoft.Testing.Extensions.TrxReport
--collect "Code Coverage"--coverageMicrosoft.Testing.Extensions.CodeCoverage
--collect "XPlat Code Coverage"--coverage --coverage-output-format coberturaMicrosoft.Testing.Extensions.CodeCoverage
--blame-hang-timeout 5m--hangdump --hangdump-timeout 5mMicrosoft.Testing.Extensions.HangDump
--blame-crash--crashdumpMicrosoft.Testing.Extensions.CrashDump
--results-directory--results-directory組み込み

執筆時点の最新バージョンは、TrxReport、HangDump、CrashDump が 2.4.1、CodeCoverage が 18.11.2 です。完全な対応表は MTP CLI オプションのリファレンスにあります。--filter は MSTest と NUnit では VSTest の式構文のまま使えるため、通常は変更する必要がありません。

3. オプションを受け取るすべてのプロジェクトで拡張を参照する

再現環境では、ソリューション全体への --report-trx が失敗したのは、xUnit プロジェクトにレポーターがなかったことだけが原因でした。それを追加すると実行が通りました。

<!-- XTests/XTests.csproj, xunit.v3.mtp-v2 4.0.1 -->
<ItemGroup>
  <PackageReference Include="xunit.v3.mtp-v2" Version="4.0.1" />
  <PackageReference Include="Microsoft.Testing.Extensions.TrxReport" Version="2.4.1" />
</ItemGroup>
MsTests.dll (net10.0|arm64) passed (837ms)
XTests.dll (net10.0|arm64) passed (922ms)
Test run summary: Passed!
  total: 4

すべてのテストプロジェクトで TRX とカバレッジを出力したい場合は、IsTestProject を条件にした Directory.Build.props に参照を置くと、新しいプロジェクトも自動的に取り込まれます。MSTest.Sdk プロジェクトにはすでにこれらが含まれているため、参照の重複を避けたい場合は条件 '$(UsingMSTestSdk)' != 'true' を追加してください。

4. TestingPlatformCommandLineArguments でオプションをプロジェクトごとに振り分ける

拡張をすべてのプロジェクトに入れたくない場合もあります。統合テストプロジェクトのカバレッジはたいていノイズになりますし、ダンプはハングするプロジェクトでだけ役に立ちます。dotnet test のコマンドラインでオプションを渡す代わりに、それを理解するプロジェクトの中に書きます。

<!-- MsTests/MsTests.csproj, MSTest.Sdk 4.4.1 -->
<Project Sdk="MSTest.Sdk/4.4.1">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <TestingPlatformCommandLineArguments>$(TestingPlatformCommandLineArguments) --coverage --coverage-output-format cobertura</TestingPlatformCommandLineArguments>
  </PropertyGroup>
</Project>

これを設定すると、素の dotnet test --solution All.sln が終了コード 0 を返し、xUnit プロジェクトはカバレッジなしで実行され、MSTest プロジェクトは TestResults に .cobertura.xml を書き出しました。Microsoft はテストフレームワークや拡張が混在するソリューションに対して同じ方法を文書化しています。Directory.Build.props の値が上書きされないよう、$(TestingPlatformCommandLineArguments) は先頭に置いてください。

5. 名前だけでなく値も検証する

オプションが --help に載っているのに終了コード 5 になる場合は、値が間違っています。再現環境では、存在しないファイルを指す --settings x.runsettings が両方のプロジェクトを終了コード 5 で失敗させました。パスはテストプロセスの作業ディレクトリから解決されますが、CI ではそれがリポジトリのルートとは限りません。MTP では時間の値に単位が必要です。--timeout 30m は動作しますが、数字だけでは動作しません。

終了コード 8: 本当にテストが 0 件だった場合

終了コードが 8 の場合、引数は受け入れられており、フィルターまたはプロジェクトの結果としてテストがなかっただけです。MTP はこれをデフォルトで失敗として扱います。0 を返していた VSTest とは異なります。対処は 3 つあります。

# .NET 10 SDK 10.0.302, MTP 2.4.1

# Accept an empty run for this invocation (returns 0)
dotnet test --project MsTests --filter "TestCategory=Nightly" --ignore-exit-code 8

# Or demand a floor: fewer than 50 tests returns exit code 9
dotnet test --solution All.sln --minimum-expected-tests 50

--ignore-exit-code は環境変数 TESTINGPLATFORM_EXITCODE_IGNORE からも読み取られるため、同じフィルターを多数のパイプラインで実行する場合に便利です。使うのは控えめにしてください。何にも一致しないフィルターが黙って通ってしまうことこそ、終了コード 8 が検出するために設計されたバグです。

複数プロジェクトの実行では、SDK のバージョンが影響します。SDK 10.0.302 では、一方のプロジェクトがフィルターに一致し、もう一方が何にも一致しなかったソリューション実行は、全体として 8 を返しました。.NET 11 RC 1 SDK では、同じコマンドが Test run summary: Passed! とともに 0 を返しつつ、空のモジュールの横には Exit code: 8 を表示しました。これは .NET 11 SDK で新しくなった実行全体でのテスト 0 件の判定です。同じフィルターで CI が .NET 10 では赤く、.NET 11 では緑になる場合は、これが理由です。終了コード 5 は変わっていません。どちらの SDK でも、いずれかのモジュールが引数を拒否すると実行全体が失敗します。

落とし穴と紛らわしい症状

関連記事

参考資料

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る