この記事では、Microsoft.Testing.Platform 用のカスタム テスト フレームワークを作成する方法について説明します。 テスト フレームワークは、唯一の必須拡張機能です。 テストを検出して実行し、結果をプラットフォームに報告します。
完全な拡張ポイントの概要とプロセス内/プロセス外の概念については、「 カスタム拡張機能の作成」を参照してください。
既存の VSTest ベースのテスト フレームワークを移行する場合は、 インターフェイスをネイティブに実装することをお勧めします。 VSTest Bridge 拡張機能は移行手順として使用できますが、ネイティブ実装では最適なエクスペリエンスが提供されます。
テストフレームワークの拡張
テスト フレームワークは、テスト プラットフォームにテストを検出して実行する機能を提供する主要な拡張機能です。 テスト フレームワークは、テストの結果をテスト プラットフォームに戻す役割を担います。 テスト フレームワークは、テスト セッションを実行するために必要な唯一の必須の拡張機能です。
テスト フレームワークを登録する
このセクションでは、テスト フレームワークをテスト プラットフォームに登録する方法について説明します。 のドキュメントに示すように、 API を使用して、テスト アプリケーション ビルダーごとに 1 つのテスト フレームワークのみを登録します。
登録 API は次のように定義されます。
ITestApplicationBuilder RegisterTestFramework(
Func<IServiceProvider, ITestFrameworkCapabilities> capabilitiesFactory,
Func<ITestFrameworkCapabilities, IServiceProvider, ITestFramework> adapterFactory);
API には、次の 2 つのファクトリが必要です。
: これは、 インターフェイスを実装するオブジェクトを受け入れ、 インターフェイスを実装するオブジェクトを返すデリゲートです。 は、構成、ロガー、コマンド ライン引数といったプラットフォーム サービスへのアクセスを提供します。
インターフェイスは、テスト フレームワークでサポートされる機能をプラットフォームと拡張機能に通知するために使用されます。 これにより、特定の動作を実装およびサポートすることで、プラットフォームと拡張機能は適切に対話できるようになります。 機能の概念について理解を深めるには、それぞれのセクションを参照してください。
: これは、ITestFrameworkCapabilities オブジェクトを受け取るデリゲートであり、このオブジェクトは によって返されるインスタンスです。また、IServiceProvider を使用してプラットフォーム サービスへのアクセスを再提供します。 予期される戻りオブジェクトは、ITestFramework インターフェイスを実装するオブジェクトです。 は、テストを検出して実行してから結果をテスト プラットフォームに伝える実行エンジンとして機能します。
プラットフォームが の作成と ITestFramework の作成を分離する必要があるのは、サポートされている機能が現在のテスト セッションを実行するのに十分でない場合に、テスト フレームワークの作成を回避するよう最適化するためです。
次のユーザー コードの例を考えてみましょう。この例は、空の機能セットを返すテスト フレームワークの登録を示しています。
internal class TestingFrameworkCapabilities : ITestFrameworkCapabilities
{
public IReadOnlyCollection<ITestFrameworkCapability> Capabilities => [];
}
internal class TestingFramework : ITestFramework
{
public TestingFramework(ITestFrameworkCapabilities capabilities, IServiceProvider serviceProvider)
{
// ...
}
// Omitted for brevity...
}
public static class TestingFrameworkExtensions
{
public static void AddTestingFramework(this ITestApplicationBuilder builder)
{
builder.RegisterTestFramework(
_ => new TestingFrameworkCapabilities(),
(capabilities, serviceProvider) => new TestingFramework(capabilities, serviceProvider));
}
}
// ...
次に、登録コードを使用して、この例の対応するエントリ ポイントについて考えてみましょう。
var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);
// Register the testing framework
testApplicationBuilder.AddTestingFramework();
using var testApplication = await testApplicationBuilder.BuildAsync();
return await testApplication.RunAsync();
注
空の ITestFrameworkCapabilities を返しても、テスト セッションの実行が妨げられることはありません。 すべてのテスト フレームワークは、テストを検出して実行できる必要があります。 その影響は、テスト フレームワークに特定の機能がない場合にオプトアウトする可能性がある拡張機能に限定する必要があります。
テスト フレームワークを作成する
は、テスト フレームワークを提供する拡張機能によって実装されます。
public interface ITestFramework : IExtension
{
Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context);
Task ExecuteRequestAsync(ExecuteRequestContext context);
Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context);
}
インターフェイス
インターフェイスは、 インターフェイスを継承します。これは、すべての拡張ポイントが継承するインターフェイスです。 は、拡張機能の名前と説明を取得するために使用されます。 また は、 を通じて、セットアップで拡張機能を動的に有効または無効にする方法も提供します。 無効にする特定の要件がない場合は、このメソッドから を返すようにしてください。
メソッド
メソッドはテスト セッションの開始時に呼び出され、テスト フレームワークの初期化に使用されます。 API は オブジェクトを受け入れ、 を返します。
public sealed class CreateTestSessionContext : TestSessionContext
{
public CancellationToken CancellationToken { get; }
}
プロパティは、から継承されます (「TestSessionContext」セクションを参照)。 を使用して、 の実行を停止します。
戻りオブジェクトは、次のように になります。
public sealed class CreateTestSessionResult
{
public string? WarningMessage { get; set; }
public string? ErrorMessage { get; set; }
public bool IsSuccess { get; set; }
}
プロパティは、セッションの作成が成功したかどうかを示すために使用されます。 が返されると、テストの実行は停止します。
メソッド
メソッドは機能的には と並列で、オブジェクト名だけが異なります。 詳細については、「」のセクションを参照してください。
メソッド
メソッドは、型 のオブジェクトを受け入れます。 このオブジェクトには、名前からもわかるように、テスト フレームワークでの実行が想定されているアクションの詳細が格納されています。 の定義は次のとおりです。
public sealed class ExecuteRequestContext
{
public IRequest Request { get; }
public IMessageBus MessageBus { get; }
public CancellationToken CancellationToken { get; }
public void Complete();
}
: これは、任意の種類の要求に対応した基本インターフェイスです。 テスト フレームワークは、次のようなライフサイクルのインプロセス ステートフル サーバーと考える必要があります。
テスト フレームワークのライフサイクルを表すシーケンス図。
上の図は、テスト フレームワーク インスタンスの作成後にテスト プラットフォームが 3 つの要求を発行することを示しています。 テスト フレームワークはこれらの要求を処理し、要求自体に含まれている サービスを利用して、特定の要求ごとの結果を提供します。 特定の要求が処理されると、テスト フレームワークはその要求に対して メソッドを呼び出し、要求が実行されたことがテスト プラットフォームに通知されます。 テスト プラットフォームは、ディスパッチされたすべての要求を監視します。 すべての要求が実行されると、インスタンスの を呼び出して破棄します ( が実装されている場合)。 要求とその完了が重複して、要求の同時実行と非同期実行が有効になる可能性があることは明らかです。
注
現在、テスト プラットフォームでは重複する要求は送信されず、次の要求を送信する前に要求 の完了を待ちます。 ただし、この動作は、今後変更される可能性があります。 同時要求のサポートは、機能システムを通じて決定されます。
の実装により、実行する必要のある正確な要求が指定されます。 テスト フレームワークは要求の種類を識別し、それに応じて処理します。 要求の種類が認識されない場合は、例外が発生します。
使用可能な要求の詳細については、IRequest セクションを参照してください。
: このサービスは、要求にリンクされているため、テスト フレームワークは、進行中の要求に関する情報を非同期でテスト プラットフォームに発行できます。 メッセージ バスはプラットフォームの中央ハブとして機能し、すべてのプラットフォーム コンポーネントと拡張機能間の非同期通信を支援します。 テスト プラットフォームに公開される可能性のある情報の包括的な一覧については、IMessageBus セクションを参照してください。
: このトークンは、特定の要求の処理を中断するために使用されます。
: 前のシーケンスで示したように、 メソッドは、要求が正常に処理され、関連するすべての情報が IMessageBus に送信されたことをプラットフォームに通知します。
Warnung
要求で の呼び出しを無視すると、テスト アプリケーションが応答しなくなります。
テスト フレームワークを自分自身の要件またはユーザーの要件に合わせてカスタマイズするには、構成ファイル内のパーソナライズされたセクションを使用するか、カスタム コマンド ライン オプションを使用します。
要求の処理
次のセクションでは、テスト フレームワークが受信して処理する可能性のあるさまざまな要求について詳しく説明します。
次のセクションに進む前に、テスト実行情報をテスト プラットフォームに伝えるための重要なサービス、IMessageBus の概念について十分理解することが重要です。
TestSessionContext
はすべての要求の共有プロパティで、進行中のテスト セッションに関する情報を提供します。
public class TestSessionContext
{
public SessionUid SessionUid { get; }
}
public readonly struct SessionUid(string value)
{
public string Value { get; }
}
は、進行中のテスト セッションの一意識別子である で構成され、テスト セッション データのログ作成と関連付けを支援します。
DiscoverTestExecutionRequest(テスト実行リクエストの検出)
public class DiscoverTestExecutionRequest
{
public TestSessionContext Session { get; }
public ITestExecutionFilter Filter { get; }
}
は、テスト フレームワークにテストを検出し、この情報を IMessageBus に伝えるよう指示します。
前のセクションで説明したように、検出されたテストのプロパティは です。 参照用の汎用コード スニペットを次に示します。
var testNode = new TestNode
{
Uid = GenerateUniqueStableId(),
DisplayName = GetDisplayName(),
Properties = new PropertyBag(
DiscoveredTestNodeStateProperty.CachedInstance),
};
await context.MessageBus.PublishAsync(
this,
new TestNodeUpdateMessage(
discoverTestExecutionRequest.Session.SessionUid,
testNode));
// ...
テスト実行要求を実行
public class RunTestExecutionRequest
{
public TestSessionContext Session { get; }
public ITestExecutionFilter Filter { get; }
}
は、テスト フレームワークにテストを実行し、この情報を IMessageBus に伝えるよう指示します。
参照用の汎用コード スニペットを次に示します。
var skippedTestNode = new TestNode()
{
Uid = GenerateUniqueStableId(),
DisplayName = GetDisplayName(),
Properties = new PropertyBag(
SkippedTestNodeStateProperty.CachedInstance),
};
await context.MessageBus.PublishAsync(
this,
new TestNodeUpdateMessage(
runTestExecutionRequest.Session.SessionUid,
skippedTestNode));
// ...
var successfulTestNode = new TestNode()
{
Uid = GenerateUniqueStableId(),
DisplayName = GetDisplayName(),
Properties = new PropertyBag(
PassedTestNodeStateProperty.CachedInstance),
};
await context.MessageBus.PublishAsync(
this,
new TestNodeUpdateMessage(
runTestExecutionRequest.Session.SessionUid,
successfulTestNode));
// ...
var assertionFailedTestNode = new TestNode()
{
Uid = GenerateUniqueStableId(),
DisplayName = GetDisplayName(),
Properties = new PropertyBag(
new FailedTestNodeStateProperty(assertionException)),
};
await context.MessageBus.PublishAsync(
this,
new TestNodeUpdateMessage(
runTestExecutionRequest.Session.SessionUid,
assertionFailedTestNode));
// ...
var failedTestNode = new TestNode()
{
Uid = GenerateUniqueStableId(),
DisplayName = GetDisplayName(),
Properties = new PropertyBag(
new ErrorTestNodeStateProperty(ex.InnerException!)),
};
await context.MessageBus.PublishAsync(
this,
new TestNodeUpdateMessage(
runTestExecutionRequest.Session.SessionUid,
failedTestNode));
データ
IMessageBus セクションで説明したように、メッセージ バスを使用する前に、提供するデータの型を指定する必要があります。 テストプラットフォームは、 の概念を表現するために、既知の型である を定義しています。
ドキュメントのこのパートでは、このペイロード データを利用する方法について説明します。 表面を確認しましょう。
public sealed class TestNodeUpdateMessage(
SessionUid sessionUid,
TestNode testNode,
TestNodeUid? parentTestNodeUid = null)
{
public TestNode TestNode { get; }
public TestNodeUid? ParentTestNodeUid { get; }
}
public class TestNode
{
public required TestNodeUid Uid { get; init; }
public required string DisplayName { get; init; }
public PropertyBag Properties { get; init; } = new();
}
public sealed class TestNodeUid(string value);
public sealed partial class PropertyBag
{
public PropertyBag();
public PropertyBag(params IProperty[] properties);
public PropertyBag(IEnumerable<IProperty> properties);
public int Count { get; }
public void Add(IProperty property);
public bool Any<TProperty>();
public TProperty? SingleOrDefault<TProperty>();
public TProperty Single<TProperty>();
public TProperty[] OfType<TProperty>();
public IEnumerable<IProperty> AsEnumerable();
public IEnumerator<IProperty> GetEnumerator();
...
}
public interface IProperty
{
}
: は と の 2 つのプロパティで構成されます。 はテストが親テストを持つ可能性を示し、テストツリーの概念を導入しています。を互いに関連付けて配置することができます。 この構造により、ノード間のツリー関係に基づいて、将来の機能強化と機能が可能になります。 テスト フレームワークでテスト ツリー構造が必要ない場合は、それを使用せず、null に設定するだけで、 の単純なフラット リストを作成できます。
: は 3 つのプロパティで構成され、そのうちの 1 つは型 の です。 この は、ノードの一意の安定した ID として機能します。 "一意の安定した ID" という用語は、同じ が異なる実行間とシステム間で同一の を維持する必要があることを意味します。 この は、テスト プラットフォームがそのまま受け入れる任意の不透明な文字列です。
Important
ID の安定性と一意性は、テスト ドメインでは重要です。 これらは、実行する単一テストの正確なターゲット設定を可能にし、ID がテストの永続的な識別子として機能できるようにして、強力な拡張機能や機能を支援します。
2 番目のプロパティは で、テストのわかりやすい名前になります。 たとえば、 コマンド ラインを実行する際にこの名前が表示されます。
3 番目の属性は で、 型になります。 コード内に示されているように、これは に関する一般的なプロパティを保持する特殊なプロパティ バッグです。 これは、プレースホルダー インターフェイス を実装するノードに任意のプロパティを追加できることを意味します。
テスト プラットフォームは、 に追加された特定のプロパティを識別し、テストが合格したか、失敗したか、またはスキップされたかを判断します。
使用可能なプロパティの現在の一覧については、「TestNodeUpdateMessage.TestNode 」セクション相対説明を参照してください。
型は、通常、すべての でアクセスでき、プラットフォームと拡張機能でクエリの実行ができるその他のプロパティを格納するために使用されます。 このメカニズムにより、破壊的変更を導入することなく、新しい情報でプラットフォームを強化できます。 コンポーネントはプロパティを認識すると、そのクエリを実行できます。認識しない場合は、無視します。
最後に、このセクションでは、テスト フレームワークの実装には、次のサンプル内のような を生成する を実装する必要があることについて説明します。
internal sealed class TestingFramework
: ITestFramework, IDataProducer
{
// ...
public Type[] DataTypesProduced =>
[
typeof(TestNodeUpdateMessage)
];
// ...
}
実行中にテスト アダプターがファイルの公開を必要とする場合は、認識されるプロパティは次のソース ファイルで確認できます。 ご覧のように、ファイル資産を一般的な方法で提供したり、特定の と関連付けたりすることができます。 をプッシュする場合は、以下に示すように、事前にプラットフォームに宣言する必要があります。
internal sealed class TestingFramework
: ITestFramework, IDataProducer
{
// ...
public Type[] DataTypesProduced =>
[
typeof(TestNodeUpdateMessage),
typeof(SessionFileArtifact)
];
// ...
}
既知のプロパティ
要求セクションで詳しく説明したように、テスト プラットフォームでは、の状態 (成功、失敗、スキップなど) を判断するために、に追加された特定のプロパティが識別されます。 これにより、ランタイムは、失敗したテストの一覧と失敗したテストに対応する情報をコンソールで正確に表示したり、テスト プロセスに適切な終了コードを設定したりできるようになります。
このセグメントでは、さまざまな既知の オプションとそれぞれの影響について説明します。
既知のプロパティの包括的な一覧については、 TestNodeProperties.csを参照してください。 プロパティの説明が見つからない場合は、問題を提出してください。
これらのプロパティは、次のカテゴリに分類できます。
- 一般的な情報: 任意の種類の要求に含めることができるプロパティ。
- 検出情報: 検出要求中に指定されるプロパティ。
- 実行情報: テスト実行要求 中に指定されるプロパティ。
特定のプロパティは必須ですが、その他のプロパティは省略可能です。 必須プロパティは、失敗したテストを報告したり、テスト セッション全体が成功したかどうかを示したりするなど、基本的なテスト機能を提供するために必要です。
一方、省略可能なプロパティは、追加情報を提供することでテスト エクスペリエンスを強化します。 これらは特に、IDE シナリオ (VS、VSCode など) や、コンソールの実行、または正しく機能するためにより詳細な情報を必要とする特定の拡張機能をサポートする際に有用です。 ただし、これらの省略可能なプロパティは、テストの実行には影響しません。
注
拡張機能は、正しく動作するために特定の情報が必要な場合、例外を通知して管理する役割を担います。 拡張機能は、必要な情報が欠落している場合、テスト実行の失敗を引き起こすのではなく、単にオプトアウトします。
一般的な情報
public record KeyValuePairStringProperty(
string Key,
string Value)
: IProperty;
は、一般的なキーと値のペア データを表します。
public record struct LinePosition(
int Line,
int Column);
public record struct LinePositionSpan(
LinePosition Start,
LinePosition End);
public abstract record FileLocationProperty(
string FilePath,
LinePositionSpan LineSpan)
: IProperty;
public sealed record TestFileLocationProperty(
string FilePath,
LinePositionSpan LineSpan)
: FileLocationProperty(FilePath, LineSpan);
は、ソース ファイル内のテストの場所を特定するために使用されます。 これは、イニシエーターが Visual Studio や Visual Studio Code などの IDE である場合に特に便利です。
public sealed record TestMethodIdentifierProperty(
string AssemblyFullName,
string Namespace,
string TypeName,
string MethodName,
string[] ParameterTypeFullNames,
string ReturnTypeFullName)
は、テスト メソッドの一意識別子です。
public sealed record TestMetadataProperty(
string Key,
string Value)
は の特性またはを伝えるために使用されます。
検出情報
public sealed record DiscoveredTestNodeStateProperty(
string? Explanation = null)
{
public static DiscoveredTestNodeStateProperty CachedInstance { get; }
}
は、TestNode が検出されたことを示します。 これは、 がテスト フレームワークに送信される際に使用されます。 プロパティで提供される便利なキャッシュ値を書き留めます。 このプロパティは必須です。
実行情報
public sealed record InProgressTestNodeStateProperty(
string? Explanation = null)
{
public static InProgressTestNodeStateProperty CachedInstance { get; }
}
は、 の実行がスケジュールされており、現在進行中であることをテスト プラットフォームに通知します。 プロパティで提供される便利なキャッシュ値を書き留めます。
public readonly record struct TimingInfo(
DateTimeOffset StartTime,
DateTimeOffset EndTime,
TimeSpan Duration);
public sealed record StepTimingInfo(
string Id,
string Description,
TimingInfo Timing);
public sealed record TimingProperty : IProperty
{
public TimingProperty(TimingInfo globalTiming)
: this(globalTiming, [])
{
}
public TimingProperty(
TimingInfo globalTiming,
StepTimingInfo[] stepTimings)
{
GlobalTiming = globalTiming;
StepTimings = stepTimings;
}
public TimingInfo GlobalTiming { get; }
public StepTimingInfo[] StepTimings { get; }
}
は、 の実行に関するタイミングの詳細をリレーするために使用されます。 また、 を介して個々の実行ステップのタイミングを可能にします. これは特に、初期化、実行、クリーンアップなど、テストの概念が複数のフェーズに分かれている場合に有用です。
次のプロパティのうち 1 つだけが必要であり、ごとに使用しての結果をテストプラットフォームに伝達します。
public sealed record PassedTestNodeStateProperty(
string? Explanation = null)
: TestNodeStateProperty(Explanation)
{
public static PassedTestNodeStateProperty CachedInstance
{ get; } = new PassedTestNodeStateProperty();
}
は、この が渡されたことをテスト プラットフォームに通知します。 プロパティで提供される便利なキャッシュ値を書き留めます。
public sealed record SkippedTestNodeStateProperty(
string? Explanation = null)
: TestNodeStateProperty(Explanation)
{
public static SkippedTestNodeStateProperty CachedInstance
{ get; } = new SkippedTestNodeStateProperty();
}
は、この がスキップされたことをテスト プラットフォームに通知します。 プロパティで提供される便利なキャッシュ値を書き留めます。
public sealed record FailedTestNodeStateProperty : TestNodeStateProperty
{
public FailedTestNodeStateProperty()
: base(default(string))
{
}
public FailedTestNodeStateProperty(string explanation)
: base(explanation)
{
}
public FailedTestNodeStateProperty(
Exception exception,
string? explanation = null)
: base(explanation ?? exception.Message)
{
Exception = exception;
}
public Exception? Exception { get; }
}
は、この がアサーションの後に失敗したことをテスト プラットフォームに通知します。
public sealed record ErrorTestNodeStateProperty : TestNodeStateProperty
{
public ErrorTestNodeStateProperty()
: base(default(string))
{
}
public ErrorTestNodeStateProperty(string explanation)
: base(explanation)
{
}
public ErrorTestNodeStateProperty(
Exception exception,
string? explanation = null)
: base(explanation ?? exception.Message)
{
Exception = exception;
}
public Exception? Exception { get; }
}
は、この が失敗したことをテスト プラットフォームに通知します。 この種類のエラーは、アサーション エラーに使用される とは異なります。 たとえば、テスト初期化エラーなどの問題を を使用して報告できます。
public sealed record TimeoutTestNodeStateProperty : TestNodeStateProperty
{
public TimeoutTestNodeStateProperty()
: base(default(string))
{
}
public TimeoutTestNodeStateProperty(string explanation)
: base(explanation)
{
}
public TimeoutTestNodeStateProperty(
Exception exception,
string? explanation = null)
: base(explanation ?? exception.Message)
{
Exception = exception;
}
public Exception? Exception { get; }
public TimeSpan? Timeout { get; init; }
}
は、この がタイムアウトの理由で失敗したことをテスト プラットフォームに通知します。 プロパティを使用して、タイムアウトを報告できます。
public sealed record CancelledTestNodeStateProperty : TestNodeStateProperty
{
public CancelledTestNodeStateProperty()
: base(default(string))
{
}
public CancelledTestNodeStateProperty(string explanation)
: base(explanation)
{
}
public CancelledTestNodeStateProperty(
Exception exception,
string? explanation = null)
: base(explanation ?? exception.Message)
{
Exception = exception;
}
public Exception? Exception { get; }
}
は、この がキャンセルにより失敗したことをテスト プラットフォームに通知します。
.NET