Testing

Test Explorer の Microsoft.Testing.Platform (MTP):C# と F# の dotnet test、xUnit v3、MSTest、NUnit

共通の台座に並ぶアイボリーの陶製タイルと3つのエメラルド色のチェックマークで、ソフトウェアテストの成功を表現

dotnet test の仕組みが変わりつつあります。Microsoft.Testing.Platform (MTP) は VSTest に代わる新しいテストプラットフォームです。テストプロジェクトは、自分でテストを検出・実行する実行可能なテストモジュールとしてビルドされます。別の testhost プロセスや中間のアダプターはありません。MSTest、NUnit、xUnit はいずれも MTP ランナーを提供し、xUnit v3 4.0 が対応するのは MTP v2 だけです。VSTest アダプターはありません。

そのため、VSTest にしか対応しないエディターの Test Explorer では、新しい xUnit v3 プロジェクトのツリーが空になります。テストは存在し、dotnet test では実行できます。それでもエディターには表示されません。

SharpLsp の Test Explorer は両方に対応しました。この機能は現在 main にあり、次のリリースに含まれます。C# と F# の xUnit v3、MSTest、NUnit の MTP テストモジュールを検出、実行、デバッグし、コードカバレッジを収集します。VSTest プロジェクトとの並行利用は、両者を含むマルチルートワークスペースでも動作します。この記事では次を説明します。

Valentin Dide さんに感謝

この機能は、Valentin Dide (@validide) さんが空のツリーに遭遇し、issue #249「Test Explorer の Microsoft.Testing.Platform (MTP) 対応」を報告してくれたことから始まりました。さらに PR #250「Test Explorer の Microsoft.Testing.Platform 対応」で、次の実装も担ってくれました。

この記事で説明する作業はすべて、その PR の上に成り立っています。Valentin さん、ありがとうございます。オープンソースの .NET ツール開発の力を示す貢献です。

MTP と VSTest の違い

VSTest では、dotnet test がプロジェクトをビルドし、アセンブリをテストホストに渡します。ホストはフレームワークのアダプター(xunit.runner.visualstudio、NUnit3TestAdapter、MSTest.TestAdapter)を読み込み、アダプターがテストを検出・実行します。

Microsoft.Testing.Platform では、テストプロジェクト自身がランナーです。独自のコマンドラインを持つ実行可能なモジュールとしてビルドされます。

TRX やカバレッジは拡張機能です。モジュールが参照・登録する NuGet パッケージであり、プラットフォームに常備されたフラグではありません。この違いが、以降の挙動の多くを説明します。

オプトイン方法:C# と F#

.NET 10 SDK では、dotnet test が global.json から実行モードを選びます。

{
  "test": {
    "runner": "Microsoft.Testing.Platform"
  }
}

SharpLsp もまずこの設定を読みます。global.json がないプロジェクトも検出します。MSBuild に Testing Platform アプリケーションかどうかを問い合わせるため、VSTest のリポジトリにある MTP プロジェクトのテストも表示されます。

MTP を使う C# xUnit v3 テストプロジェクトの例です。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <OutputType>Exe</OutputType>
    <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="xunit.v3" Version="4.0.0" />
    <PackageReference Include="Microsoft.Testing.Extensions.TrxReport" Version="2.4.0" />
  </ItemGroup>
</Project>

F# xUnit v3 テストプロジェクトでは、同じプロジェクト設定に F# 固有のコンパイル順序を加えます。

<ItemGroup>
  <Compile Include="Tests.fs" />
</ItemGroup>
module MyApp.Tests.Calculator

open Xunit

[<Fact>]
let ``adds two numbers`` () = Assert.Equal(3, 1 + 2)

[<Theory>]
[<InlineData(1, 2, 3)>]
[<InlineData(2, 2, 4)>]
let ``adds rows`` (a: int) (b: int) (expected: int) = Assert.Equal(expected, a + b)

MTP の MSTest では MSTest 4.x を参照し、<EnableMSTestRunner>true</EnableMSTestRunner> を設定します。NUnit では NUnit と NUnit3TestAdapter 6.x を参照し、<EnableNUnitRunner>true</EnableNUnitRunner> を設定します。いずれもモジュール自身が実行ファイルなので <OutputType>Exe</OutputType> が必要です。

xUnit v3 と NUnit では Microsoft.Testing.Extensions.TrxReport を参照してください(MSTest には既に含まれます)。これがないとモジュールは --report-trx を拒否し、テストごとの結果を報告できません。その場合、Test Explorer は「No result reported」を並べる代わりに、追加すべきパッケージ名を示します。

Test Explorer が MTP モジュールに対して行うこと

検出。 SharpLsp は検出対象をビルドしてから、dotnet exec <module> --list-tests json で各モジュールに直接問い合わせます。dotnet test は json 引数を転送しないため、経由しません(dotnet/sdk#49754)。転送されるテキスト形式には表示名しかなく、MSTest では単なるメソッド名になります。

JSON の一覧には各テストの名前空間、型、メソッド、ソース位置が含まれるため、MTP のテスト行から記述された行に移動できます。ツリーは VSTest と同じ Assembly → Namespace → Class → Test です。

Testing Platform 2.3 より古いモジュールは JSON で列挙できません。その場合は、フレームワークパッケージの更新を促すメッセージをログとツリーに表示します。

実行(▶)。 選択全体につきモジュールごとに 1 回だけ起動し、テストごとには起動しません。20 テストあるクラスでもプロセス起動は 20 回になりません。選択内容は --filter-uid 値で渡します。これは対応するすべての MTP フレームワークが受け付ける dotnet test のフィルターであり、エスケープなしのリテラル値を使えます。NUnit の Adds_Case(2,2,4) のような uid で重要です。

結果は --report-trx から取得し、VSTest と同じ TRX リーダーで読み取ります。成功、失敗、スキップは実際の結果で、アサーション文もフレームワーク自身のものです。データ駆動テスト(xUnit の [Theory]、MSTest の [DataRow]、NUnit の [TestCase])はツリーでは 1 行となり、最も悪いデータ行の結果で判定されます。

コードカバレッジ。 モジュールが Microsoft.Testing.Extensions.CodeCoverage を参照している場合、Run with Coverage は --coverage --coverage-output-format cobertura を渡します。MTP は結果ディレクトリ直下に <guid>.cobertura.xml を書きます。coverlet の coverage.cobertura.xml は 1 階層下です。リーダーは各収集器が実際に書く深さを参照します。

MTP で「dotnet test code coverage」を探しているなら、答えはそのパッケージと 2 つのフラグです。パッケージのないモジュールには、その名前を示すメッセージが出ます。

デバッグ。 Debug プロファイルは TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1 でモジュールを起動します。モジュールは pid を出力して待機し、SharpLsp は netcoredbg ベースのデバッガーをそのプロセスに接続します。テスト本体のブレークポイントで停止し、Just My Code、ローカル変数、ウォッチが機能します。デバッグ実行がツリーに架空の結果を書き込むことはありません。

VSTest と MTP の併用。 ランナーはワークスペース単位ではなく、検出対象ごとに選ばれます。VSTest フォルダーと MTP フォルダーを持つマルチルートワークスペースでは、各テストが検出元のランナーに送られます。MTP モジュールは実行前にそれぞれのフォルダーから再ビルドされます。

F# も第一級の対応

SharpLsp は F# を第一級の言語として扱います。そのため、MTP の各ケースを C# だけでなく F# でも確認しています。

信頼性のために修正した不具合

デモプロジェクトで MTP を「動かす」ことは簡単です。実際のプロジェクトで正しい結果を出すのは別の話です。レビューと強化の過程で実際の不具合が見つかり、それぞれ失敗するエンドツーエンドテストを先に書いてから根本原因を修正しました。

MTP 上の xUnit、MSTest、NUnit

新しいプロジェクトで xUnit と MSTest、または NUnit と xUnit を比較するなら、エディターから見た Microsoft.Testing.Platform 上の違いは次のとおりです。

xUnit v3 MSTest NUnit
VSTest アダプター 4.0 以降なし(MTP 専用) あり あり
TRX レポート Microsoft.Testing.Extensions.TrxReport を追加 組み込み Microsoft.Testing.Extensions.TrxReport を追加
--list-tests json のソース位置 あり あり なし
データ行の uid 行データのハッシュ 行の位置 修飾された名前(例:Adds_Case(2,2,4))
表示名 完全修飾名 単純なメソッド名 引数付きのメソッド名

これだけで特定のフレームワークを選ぶべきではありません。Test Explorer が表示名からテスト id を作ってはいけないこと、昨日の uid を信じてはいけないことを示しています。

実際のプロジェクトで検証

MTP 対応には、実際の VS Code 拡張ホスト内で本物の .csproj と .fsproj をビルドするエンドツーエンドテストがあります。

Windows と Linux の CI で、VSTest のスイートと並んで専用のジョブとして実行されます。

FAQ

Microsoft.Testing.Platform で dotnet test を使うには? .NET 10 SDK では global.json に "test": { "runner": "Microsoft.Testing.Platform" } を追加し、MTP 対応のフレームワークパッケージを使います(xUnit v3、EnableMSTestRunner を設定した MSTest、または EnableNUnitRunner を設定した NUnit)。

xUnit v3 は VSTest で動きますか? 4.0 以降は動きません。xunit.v3 4.x は MTP v2 専用で、VSTest アダプターはありません。

MTP でテストを絞り込むには? フレームワークごとに固有のフィルターがあります。--filter-uid はプラットフォーム自身のフィルターで、SharpLsp の ▶ でも使います。選択したテストだけを正確に実行できます。

MTP の dotnet test でコードカバレッジを取るには? Microsoft.Testing.Extensions.CodeCoverage を参照し、--coverage --coverage-output-format cobertura を渡します。SharpLsp Test Explorer の Run with Coverage が代わりに実行します。

1 つのリポジトリで VSTest と MTP を混在させられますか? はい。SharpLsp は検出対象ごとにランナーを選び、検出元のランナーへ各テストを送ります。

試してみる

MTP 対応は SharpLsp 0.21.0 の次のリリースで提供予定です。リリースページを確認するか、現在の main から VS Code 拡張機能をビルドしてください。Microsoft.Testing.Platform を使う xUnit v3、MSTest、NUnit プロジェクトを含むソリューションを開き、Testing ビューを表示します。ツリーが空、結果が違う、デバッガーが接続されないなどの問題があれば、issue を作成してください。この機能もそこから始まりました。