MTP サーバー モードを使用すると、エディター、IDE、カスタム テスト UI、またはその他のテスト ツールがテスト アプリケーションを起動し、JSON-RPC を介して検出と実行を実行できます。 ターミナル出力を解析せずに、ツールでテスト メタデータ、選択したテストの実行、進行状況の更新、または結果成果物が必要な場合は、サーバー モードを使用します。 通常のコマンド ラインと CI の実行では、テスト アプリケーションを直接実行するか、 dotnet testを使用します。
永続接続を使用しない1回限りの検出には、--list-tests json を使用します。 既存の VSTest クライアントの場合は、「プログラムによるテストの検出と実行」を参照してください。
--serverまたは--server jsonrpcでパブリック JSON-RPC サーバーを起動します。
Important
.NET SDK は、--server dotnettestcliを介して個別の内部バイナリ プロトコルを使用します。 その値を自分で渡さないでください。
dotnet test は、トランスポートとプロトコルの引数を構成します。
ソースのみのクライアントを使用する
MTP 2.4 は、正規の JSON-RPC クライアントを提供します。 Microsoft.Testing.Platform.ServerMode.Client.Sources パッケージを追加します。 パッケージは、ランタイム アセンブリを追加する代わりに、C# ソースをプロジェクトに挿入します。
ソースのみの設計では、次の機能が提供されます。
- ランタイム DLL または追加のデプロイ依存関係はありません。
- Native AOT 対応の、リフレクションを使用しないシリアル化。
- MTP サーバーと同じリポジトリからのプロトコルの種類とシリアル化コード。
挿入された MtpServerClient、 IMtpServerClient、プロトコルの種類は、接続の初期化、テストの検出、テストの実行、サーバーの終了の要求、テスト ノードの更新の報告を行うことができます。 挿入されたすべての型は、それを使用するアセンブリに対しては internal のままです。 クライアントは、プロセスの起動、トランスポート、メッセージのフレーミング、およびシリアル化を処理します。JSON-RPC を独自に実装する必要はありません。
パッケージは C# ソースコードをコンパイルします。 Visual Basic やその他の .NET 言語からクライアントを使用するには、次の例のように、注入された型に対して独自の公開 API を提供する C# クラス ライブラリを参照してください。
.NETからテストを検出して実行する
この例では、ソースのみのクライアント パッケージ バージョン 2.5.2 を使用し、.NET 10 をターゲットとしています。 最初に MTP 対応のテスト アプリケーションをビルドしてから、ビルドされた .dll または実行可能ファイルへのパスをクライアントに渡します。
共有 C# ヘルパーは、テストを検出し、各ノードのシリアル化可能なメタデータを出力し、UID によって検出されたテストを実行します。 サブセットを実行するには、検出更新から必要な UID を選択し、それらの UID のみを RunTestsAsync に渡してください。 テスト用の識別情報として表示名を使用しないでください。
using System.Text.Json;
using Microsoft.Testing.Platform.ServerMode.Client;
namespace TestClientSample;
public static class TestClient
{
public static async Task<int> DiscoverAndRunAsync(
string testApplicationPath,
CancellationToken cancellationToken = default)
{
string path = Path.GetFullPath(testApplicationPath);
var testUids = new HashSet<string>(StringComparer.Ordinal);
using (IMtpServerClient client = await MtpServerClient.LaunchAsync(
path, cancellationToken: cancellationToken))
{
try
{
MtpServerCapabilities capabilities =
await client.InitializeAsync(cancellationToken);
if (capabilities.MultiConnectionProvider)
{
throw new NotSupportedException("A passive controller connection can't discover or run tests.");
}
if (!capabilities.SupportsDiscovery)
{
throw new NotSupportedException("The test application doesn't support discovery.");
}
client.LogReceived += (_, log) => Console.Error.WriteLine(log.Message);
client.TestNodesUpdated += (_, update) =>
{
foreach (MtpTestNodeUpdate change in update.Changes)
{
Console.WriteLine(JsonSerializer.Serialize(change.Node));
if (change.NodeType != "group"
&& change.ExecutionState == "discovered"
&& change.Uid is string uid)
{
testUids.Add(uid);
}
}
};
await client.DiscoverTestsAsync(cancellationToken);
await client.ExitAsync(cancellationToken);
}
finally
{
await client.ShutdownAsync();
}
}
if (testUids.Count == 0)
{
Console.Error.WriteLine("No tests were discovered.");
return 8;
}
var completedUids = new HashSet<string>(StringComparer.Ordinal);
bool hasUnsuccessfulResult = false;
using IMtpServerClient runClient = await MtpServerClient.LaunchAsync(
path, cancellationToken: cancellationToken);
try
{
MtpServerCapabilities capabilities =
await runClient.InitializeAsync(cancellationToken);
if (capabilities.MultiConnectionProvider)
{
throw new NotSupportedException("A passive controller connection can't discover or run tests.");
}
runClient.LogReceived += (_, log) => Console.Error.WriteLine(log.Message);
runClient.TestNodesUpdated += (_, update) =>
{
foreach (MtpTestNodeUpdate change in update.Changes)
{
Console.WriteLine(JsonSerializer.Serialize(change.Node));
if (change.Uid is string uid
&& testUids.Contains(uid)
&& change.ExecutionState is string state
&& state is not ("discovered" or "in-progress")
&& (!change.Node.TryGetValue("retry.is-superseded", out object? superseded)
|| superseded is not true))
{
completedUids.Add(uid);
hasUnsuccessfulResult |= state is not ("passed" or "skipped");
}
}
};
MtpRunResult result = await runClient.RunTestsAsync(testUids, cancellationToken);
foreach (MtpAttachment artifact in result.Artifacts)
{
Console.WriteLine($"Artifact: {artifact.Uri}");
}
await runClient.ExitAsync(cancellationToken);
if (hasUnsuccessfulResult || !testUids.All(completedUids.Contains))
{
Console.Error.WriteLine("A selected test failed or didn't report a final result.");
return 2;
}
return 0;
}
finally
{
await runClient.ShutdownAsync();
}
}
}
ヘルパーは検出と実行のために別のテスト プロセスを開始するため、 MtpServerCapabilities.MultiRequestSupportは必要ありません。 プロセスを再利用するには、InitializeAsync の後で、複数の要求を発行する前に、その機能を確認します。 検出を要求する前に、 SupportsDiscovery も確認してください。
Important
ソースのみのクライアントの起動ヘルパーは、1 つの JSON-RPC 接続を管理します。 バージョン 2.5.2 では、コントローラーバックド拡張機能で使用される追加の接続は受け付けおよびルーティングされません。 たとえば、Windows 上の TRX レポートでは、アクティブなテスト ホストより前にパッシブ接続が到着するコントローラーが起動されることがあります。
MultiConnectionProviderがtrueに設定されている接続では、更新と成果物のみがプッシュされます。ディスカバリーや実行要求を送信しないでください。 この例は、その接続を拒否します。
コントローラーによるレポートの場合は、CLI または dotnet test からテスト アプリケーションを実行するか、プロトコルの追加接続を処理するツール クライアントを使用します。 単一接続クライアントでも、アクティブなテスト ホストが発行するアーティファクトを引き続き受信できます。
C# と Visual Basic の両方のコンソール プロジェクトは、C# ヘルパー プロジェクトを参照します。
using TestClientSample;
if (args.Length != 1)
{
Console.Error.WriteLine("Usage: DiscoverAndRun <test-application-path>");
return 1;
}
return await TestClient.DiscoverAndRunAsync(args[0]);
テスト アプリケーション パスを引数として使用して、いずれかのコンソール プロジェクトを実行します。
dotnet run --project <client-project> -- <test-application-path>
この例では、選択したすべてのUIDに最終的な結果があり、置き換えられていないすべての最終結果がskippedまたは2を報告する場合はpassed、最終的な結果が失敗または欠落している場合は8、検出でテストが返されない場合は0が返されます。 これらの決定は、サンプル クライアントに属します。 完了した RunTestsAsync タスクは、テストが成功したのではなく、プロトコル要求が完了したことを意味します。
MtpRunResult.Artifacts には、成功/失敗の判定ではなく、実行アーティファクトが含まれています。 test-node の更新を使用して、テスト結果を判定します。
複数の結果が1つのUIDを共有できます。 この例では、retry.is-superseded が true である場合にのみ、以前の再試行は無視されます。UID の等価性だけでは再試行は確立されません。 後で成功した結果またはスキップされた結果は、上書きされていない以前の失敗を消去できません。
更新とメタデータを取り込む
検出または実行の前に TestNodesUpdated を購読してください。 各更新プログラムから Changes を読み取り、 Uidでノードを識別します。 クライアントはDisplayName、ExecutionState、ErrorMessage、DurationInMillisecondsなどの便利なプロパティを提供します。
traitsディクショナリを読み取ります。これには、Node やフレームワーク固有のプロパティなど、他のシリアル化されたフィールドが含まれます。
MSTest は、テスト カテゴリとカスタム TestProperty 値を特性として公開します。 メタデータの可用性は、テスト フレームワークと、検出時にテスト フレームワークが公開する内容によって異なります。 任意の属性とインメモリの IProperty 実装は、プロセス境界を自動的に越えることはありません。 ポータブル キー値メタデータを発行するには、パブリック TestMetadataProperty 型を使用します。 プラットフォーム層と互換性層は、他のシリアル化可能なフィールドを提供できます。それらの内部プロパティの型は拡張 API ではありません。 詳細については、「テストノードのプロパティ」を参照してください。
JSON-RPC ノードディクショナリが--list-tests jsonドキュメントと一致することを想定しないでください。 トランスポートでは、異なるフィールド レイアウトが使用されます。 また、すべてのフレームワークが VSTest 固有の vstest.* フィールドを出力すると想定しないでください。
クライアントは更新を順番に処理し、検出または実行タスクが完了する前に対応するハンドラーを呼び出します。 最終的な更新を待つための待機時間は必要ありません。 ハンドラーはクライアントの読み取りループ上で同期的に実行されるため、ハンドラーの処理をブロックしたり、別のクライアント操作の完了を同期的に待機したりしないでください。 負荷の高い処理や UI の更新は、適切なスレッドに割り当てます。
キャンセル トークンを検出メソッドまたは実行メソッドに渡して、プロトコル経由で取り消しを要求できるようにします。 テスト フレームワークがツリー フィルターをサポートしている場合は、最初に UID コレクションを作成せずにテストを選択するには、MTP ツリー フィルター式で DiscoverTestsWithFilterAsync または RunTestsWithFilterAsync を使用します。 これらのメソッドでは、VSTest --filter 式はサポートされていません。
クライアント要件を満たす
C# 12 以降を使用します。 パッケージは、プロジェクトの言語バージョン設定を考慮します。 ターゲット フレームワークの既定の C# バージョンが古い場合は、LangVersion を 12.0 以降の値に設定します。 パッケージには、.NET Framework 4.6.2 および netstandard2.0 の内部互換型も用意されています。
内部型が重複しないようにパッケージを追加する前に、MTP クライアントの手書きコピーを削除します。
起動モデルを選択する
外部プロセスとしてテスト アプリケーションを開始するには、 MtpServerClient.LaunchAsyncを使用します。 クライアントはその子プロセスを管理し、必要に応じてteardown時にそのプロセスを終了します。
特定の環境の外部テスト アプリケーションを構成するには、起動前に MtpServerClientOptions.EnvironmentVariables を設定します。 子プロセスは親の環境を継承し、指定された値が子に適用されます。 これらのオプションは、インプロセス ホストの環境を変更しません。
各クライアントは、1 つのテスト アプリケーションを制御します。 複数のテスト アプリケーションの場合は、アプリケーションごとにクライアントを作成し、その結果をツールで集計します。
Process.Startを使用できない埋め込みホストまたは UI スレッド環境の場合は、MtpServerClient.LaunchInProcessAsyncを使用します。 このメソッドは、現在のプロセスの非同期コールバックを介してテスト アプリケーションを実行します。 コールバックとクライアントがプロセスを共有するため、起動スレッドをブロックしないでください。
どちらの起動モデルもループバック TCP トランスポートを使用します。 Browser WebAssembly は必要なリスナーを作成できないため、インプロセス起動では PlatformNotSupportedExceptionがスローされます。 インプロセス起動では、代替の WebAssembly トランスポートは提供されません。
クライアントをシャットダウンする
呼び出し元のスレッドをブロックせずにクライアントと起動されたアプリケーションを破棄するには、 IMtpServerClient.ShutdownAsync()を呼び出します。 非同期メソッドは、埋め込みホストでの UI スレッドの応答性とデッドロックの問題を回避します。
インプロセス ホストの場合、MtpServerClientOptions.ServerShutdownTimeout は、クライアントがトランスポートを閉じた後、終了処理が待機する時間を制御します。 既定値は 30 秒です。 シャットダウン後に ServerExitCode を読み取り、コールバックの終了コードを取得します。 値は、アプリケーションの実行中、または終了コードを返す代わりにアプリケーションが失敗した場合に null のままです。
クライアントのステートフル性を宣言する
MTP 2.4.1 以降では、クライアントがテスト ノードの更新情報をどのように処理するかを指定するために、null 許容の MtpServerClientOptions.IsStateful オプションを設定します:
| Value | 宣言 |
|---|---|
true |
クライアントはセッションのアドレス指定可能な一連のテスト ノードを保持し、別の更新によって変更されるまで、各ノードの最後の既知の状態を保持します。これは IDE のテスト エクスプローラーのような動作です。 |
false |
クライアントは、そのテスト ノードの状態を保持せずに更新を取り込みます。 |
null (既定値) |
クライアントはケーパビリティを宣言しません。 初期化ハンドシェイクはそのフィールドを省略します。 |
クライアントは、試験的な capabilities.testing.isStateful プロトコル機能を介して明示的なブール値を送信します。 ステートフル性は、接続の有効期間とサーバーの MultiRequestSupport 機能とは無関係です。 クライアントは、同じ接続を介して複数の要求を送信するためだけにステートフルになりません。
MTP 2.4.0 では、null 非許容のブール型オプションが使用され、既定値は false です。 未宣言の機能を維持するには、MTP 2.4.1 以降を使用してください。
フレームワークまたは拡張機能内で、試験的なIClientInfoサービスを使用して宣言を検査します。 保守的なブール値の結果については clientInfo.Capabilities.IsStateful を参照してください。機能が未宣言の場合、その結果は false です。 MTP 2.4.1 以降では、ClientCapabilitiesExtensions.GetIsStateful() は宣言されていないクライアント、明示的にステートレスなクライアント、およびステートフルなクライアントの区別を保持します。
プロトコルの互換性をネゴシエートする
MTP 2.4 以降では、サーバー モード プロトコル ネゴシエーションは MTP NuGet パッケージ バージョンから独立しています。 クライアントは、サポートされているプロトコル バージョンを宣言し、サーバーは共通のバージョンを選択します。 サポートされているバージョンが重複していない場合、テスト アプリケーションは互換性のないワイヤ コントラクトを続行するのではなく、 ProtocolError 終了コード (12) で終了します。
メッセージを検証するには、バージョン管理された サーバー モード 1.0 JSON スキーマ を使用します。 System.Text.Json シリアライザーと Jsonite シリアライザーは、同じスキーマとエラー エンベロープに従います。
こちらも参照ください
.NET