本文將介紹 MTP 在測試框架本身之外的可擴展性點。 關於測試框架的建立,請參見 「建構測試框架」。
關於完整的擴充點摘要及進程/出程概念,請參見「建立自訂擴充」。
擴展性點
測試平台提供了額外的擴充點,讓您可以自訂平台和測試框架的行為。 這些擴充點是可選的,可用於增強測試體驗。
Tip
本文中展示的每個擴充功能都包含一個手動註冊片段(例如, builder.TestHost.AddDataConsumer(...))。 如果你將擴充功能封裝成 NuGet 套件來發佈,就可以透過提供一個 TestingPlatformBuilderHook 和一個簡單的 MSBuild props 檔案,讓使用者略過手動呼叫。 接著,自動產生的進入點會自動呼叫你的 Hook。 詳情請參閱使用 TestingPlatformBuilderHook 自動註冊您的擴充功能。
ICommandLineOptionsProvider 擴充功能
備註
當您擴展此 API 時,自訂擴展將同時存在於測試宿主進程的內部和外部。
如架構區段所討論的,初始步驟涉及建立 ITestApplicationBuilder 以註冊測試框架和擴充功能。
var builder = await TestApplication.CreateBuilderAsync(args);
CreateBuilderAsync 方法接受名為 string[] 的字串陣列 (args)。 這些參數可用於將命令列選項傳遞給測試平台的所有元件 (包括內建元件、測試框架和擴充功能),從而允許自訂其行為。
通常,傳遞的參數是在標準 Main(string[] args) 方法中接收的參數。 但是,如果主機環境不同,則可以提供任意參數清單。
參數前面必須加上雙破折號 --。 例如: --filter 。
如果測試框架或擴充點等元件希望提供自訂命令列選項,則可以透過實作 ICommandLineOptionsProvider 介面來實作。 然後可以透過 ITestApplicationBuilder 屬性的註冊處理站向 CommandLine 註冊此實作,如下所示:
builder.CommandLine.AddProvider(
static () => new CustomCommandLineOptions());
在提供的範例中,CustomCommandLineOptions 是 ICommandLineOptionsProvider 介面的實作,該介面包含以下成員和資料類型:
public interface ICommandLineOptionsProvider : IExtension
{
IReadOnlyCollection<CommandLineOption> GetCommandLineOptions();
Task<ValidationResult> ValidateOptionArgumentsAsync(
CommandLineOption commandOption,
string[] arguments);
Task<ValidationResult> ValidateCommandLineOptionsAsync(
ICommandLineOptions commandLineOptions);
}
public sealed class CommandLineOption
{
public string Name { get; }
public string Description { get; }
public ArgumentArity Arity { get; }
public bool IsHidden { get; }
// ...
}
public interface ICommandLineOptions
{
bool IsOptionSet(string optionName);
bool TryGetOptionArgumentList(
string optionName,
out string[]? arguments);
}
如所觀察到的,ICommandLineOptionsProvider 擴充了 IExtension 介面。 因此,與任何其他擴充功能一樣,您可以選擇使用 IExtension.IsEnabledAsync API 啟用或停用它。
ICommandLineOptionsProvider 的執行順序為:
讓我們檢查一下 api 及其含義:
ICommandLineOptionsProvider.GetCommandLineOptions():此方法用於檢索元件提供的所有選項。 每個 CommandLineOption 都需要指定以下屬性:
string name:這是選項名稱的呈現方式,不包含破折號。 例如,篩選器將由使用者作為 --filter 使用。
string description:這是選項的描述。 當使用者將 --help 作為參數傳遞給應用程式建立器時,其就會顯示。
ArgumentArity arity:選項的元數是指定該選項或指令時可以傳遞的值的數量。 目前可用的參數數量有:
-
Zero:表示參數元數為零。 -
ZeroOrOne:表示參數元數為零或一。 -
ZeroOrMore:表示參數元數為零或以上。 -
OneOrMore:表示參數元數為一或以上。 -
ExactlyOne:表示參數元數恰好為一。
有關範例,請參閱 System.CommandLine 元數表。
bool isHidden:此屬性表示該選項可供使用,但在 --help 叫用時不會顯示在描述中。
ICommandLineOptionsProvider.ValidateOptionArgumentsAsync:此方法用於驗證使用者提供的參數。
例如,如果您有一個名為 --dop 的參數,該參數表示我們的自訂測試框架的並行程度,則使用者可能會輸入 --dop 0。 在這種情況下,值 0 將無效,因為預計其並行度為 1 或更高。 透過使用 ValidateOptionArgumentsAsync,您可以執行預先驗證並在必要時傳回錯誤訊息。
上述範例的可能實作可能是:
public Task<ValidationResult> ValidateOptionArgumentsAsync(
CommandLineOption commandOption,
string[] arguments)
{
if (commandOption.Name == "dop")
{
if (!int.TryParse(arguments[0], out int dopValue) || dopValue <= 0)
{
return ValidationResult.InvalidTask("--dop must be a positive integer");
}
}
return ValidationResult.ValidTask;
}
ICommandLineOptionsProvider.ValidateCommandLineOptionsAsync:此方法作為最後一個方法呼叫,允許進行全域一致性檢查。
例如,假設我們的測試框架能夠產生測試結果報告,並將其儲存到檔案中。 使用 --generatereport 選項存取此功能,並使用 --reportfilename myfile.rep 指定檔案名稱。 在這種情況下,如果使用者僅提供 --generatereport 選項而不指定檔案名稱,則驗證會失敗,因為沒有檔案名稱就無法產生報告。
上述範例的可能實作可能是:
public Task<ValidationResult> ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions)
{
bool generateReportEnabled = commandLineOptions.IsOptionSet(GenerateReportOption);
bool reportFileName = commandLineOptions.TryGetOptionArgumentList(ReportFilenameOption, out string[]? _);
return (generateReportEnabled || reportFileName) && !(generateReportEnabled && reportFileName)
? ValidationResult.InvalidTask("Both `--generatereport` and `--reportfilename` need to be provided simultaneously.")
: ValidationResult.ValidTask;
}
請注意,ValidateCommandLineOptionsAsync 方法提供了 ICommandLineOptions 服務,用於擷取平台本身解析的參數資訊。
ITestSessionLifetimeHandler 擴充功能
ITestSessionLifetimeHandler 是處理序內擴充功能,可以在測試工作階段之前和之後執行程式碼。
若要註冊自訂 ITestSessionLifetimeHandler,請使用以下 API:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHost.AddTestSessionLifetimeHandle(
static serviceProvider => new CustomTestSessionLifetimeHandler());
工廠利用 IServiceProvider 來存取測試平台提供的服務套件。
這很重要
註冊順序很重要,因為 API 是按照註冊順序呼叫的。
ITestSessionLifetimeHandler 介面包含以下方法:
public interface ITestSessionLifetimeHandler : ITestHostExtension
{
Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext);
Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext);
}
public interface ITestSessionContext
{
SessionUid SessionUid { get; }
CancellationToken CancellationToken { get; }
}
public readonly struct SessionUid(string value)
{
public string Value { get; } = value;
}
public interface ITestHostExtension : IExtension
{
}
這很重要
在 MTP 2.0.0 中,這兩種方法都改為接受單一的 ITestSessionContext 參數,而該參數會公開 SessionUid 和 CancellationToken。 在 MTP 1.x 中,每個方法分別取一個 SessionUid and CancellationToken 參數。 如需詳細資訊,請參閱從 Microsoft.Testing.Platform (MTP) v1 移轉至 v2。
ITestSessionLifetimeHandler 是 ITestHostExtension 的一種類型,當作所有測試主機擴充功能的基礎。 與所有其他擴充點一樣,它也繼承自 IExtension。 因此,與任何其他擴充功能一樣,您可以選擇使用 IExtension.IsEnabledAsync API 啟用或停用它。
請考慮此 API 的以下詳細資訊:
OnTestSessionStartingAsync:此方法在測試會話開始前被調用,並接收 ITestSessionContext,該 SessionUid 為目前測試會話提供不透明的識別碼。
OnTestSessionFinishingAsync:此方法在測試工作階段完成後調用,確保測試框架已完成所有測試的執行,並向平台報告所有相關資料。 通常,在此方法中,擴充功能會使用 IMessageBus 將自訂資產或資料傳輸到共用平台匯流排。 此方法還可以向任何自訂處理序外擴充功能發出測試工作階段已結束的訊號。
最後,ITestSessionContext 會公開一個 CancellationToken,而擴充功能應遵守此契約。
如果您的擴充功能需要密集初始化並且需要使用 async/await 模式,您可以參考 Async extension initialization and cleanup。 如果需要在擴充點之間共用狀態,可以參考 CompositeExtensionFactory<T> 區段。
ITestApplicationLifecycleCallbacks 擴充功能
這很重要
ITestApplicationLifecycleCallbacks 在 MTP 2.0.0 中被移除。 請改用 ITestHostApplicationLifetime。 如需詳細資訊,請參閱從 Microsoft.Testing.Platform (MTP) v1 移轉至 v2。
此 ITestHostApplicationLifetime 介面允許 進行中的 擴充功能在 測試主機的開始與結束處執行程式碼。
若要註冊自訂 ITestHostApplicationLifetime,請使用以下 API:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHost.AddTestHostApplicationLifetime(
static serviceProvider
=> new CustomTestHostApplicationLifetime());
工廠使用 IServiceProvider 來存取測試平台所提供的服務。
這很重要
註冊順序很重要,因為 API 是按照註冊順序呼叫的。
ITestHostApplicationLifetime 介面包含以下方法:
public interface ITestHostApplicationLifetime : ITestHostExtension
{
Task BeforeRunAsync(CancellationToken cancellationToken);
Task AfterRunAsync(
int exitCode,
CancellationToken cancellationToken);
}
public interface ITestHostExtension : IExtension
{
}
ITestHostApplicationLifetime 介面延伸自 ITestHostExtension,後者作為所有 測試主機擴充功能的基礎。 與所有其他擴充點一樣,它也繼承自 IExtension。 因此,與任何其他擴充功能一樣,您可以選擇使用 IExtension.IsEnabledAsync API 啟用或停用它。
BeforeRunAsync:此方法可作為測試主機的初始聯繫點,並且是處理序內擴充功能執行功能的第一個機會。 如果某個功能設計為跨兩種環境執行,則它通常用於與任何相應的處理序外擴充功能建立連接。
例如,內建的懸掛傾印功能由處理序內和處理序外擴充功能組成,而此方法用於與擴充功能的處理序外元件交換資訊。
AfterRunAsync:此方法是結束 int ITestApplication.RunAsync() 之前的最後一個呼叫,它提供了 exit code。 它應該僅用於清理工作,並通知測試主機即將終止的任何相應的處理序外擴充功能。
最後,這兩個 API 都會使用 CancellationToken,擴充功能預期應遵循該項參數。
IDataConsumer 擴充功能
這是一個IDataConsumer進行中的擴充套件,能夠訂閱並接收IData由測試框架及其擴充套件發佈至 IMessageBus 的資訊。
這個擴充點至關重要,因為它使開發人員能夠收集和處理測試工作階段期間產生的所有資訊。
若要註冊自訂 IDataConsumer,請使用以下 API:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHost.AddDataConsumer(
static serviceProvider => new CustomDataConsumer());
工廠利用 IServiceProvider 來存取測試平台提供的服務套件。
這很重要
註冊順序很重要,因為 API 是按照註冊順序呼叫的。
IDataConsumer 介面包含以下方法:
public interface IDataConsumer : IExtension
{
Type[] DataTypesConsumed { get; }
Task ConsumeAsync(
IDataProducer dataProducer,
IData value,
CancellationToken cancellationToken);
}
public interface IData
{
string DisplayName { get; }
string? Description { get; }
}
這很重要
在 MTP 2.0.0 中,IDataConsumer 已移至 Microsoft.Testing.Platform.Extensions 命名空間,現在直接繼承自 IExtension。 在 MTP 1.x 中,它擴展 ITestHostExtension了 。 你仍然會用 builder.TestHost.AddDataConsumer(...) 來註冊它。 如需詳細資訊,請參閱從 Microsoft.Testing.Platform (MTP) v1 移轉至 v2。
IDataConsumer 繼承自 IExtension。 因此,與任何其他擴充功能一樣,您可以選擇使用 IExtension.IsEnabledAsync API 啟用或停用它。
DataTypesConsumed:此屬性將回傳此擴充功能預期使用的 Type 清單。 它對應於 IDataProducer.DataTypesProduced。 值得注意的是,IDataConsumer 可以沒有任何問題地訂閱源自不同 IDataProducer 執行個體的多種類型。
ConsumeAsync:每當目前的取用者所訂閱類型的資料發佈到 IMessageBus 時,此方法就會觸發。 它會接收 IDataProducer,提供有關資料負載的生產者以及 IData 負載本身的詳細資訊。 正如您所看到的,IData 是一個通用預留位置介面,其中包含一般資訊資料。 能夠發佈不同類型的 IData,意味著使用者必須根據型別本身進行 switch 判斷,才能將其轉型為正確的型別並存取特定資訊。
需要 elaborates 由TestNodeUpdateMessage產生的 的使用者範例實作可以是:
internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer
{
public Type[] DataTypesConsumed => new[] { typeof(TestNodeUpdateMessage) };
...
public Task ConsumeAsync(
IDataProducer dataProducer,
IData value,
CancellationToken cancellationToken)
{
var testNodeUpdateMessage = (TestNodeUpdateMessage)value;
switch (testNodeUpdateMessage.TestNode.Properties.Single<TestNodeStateProperty>())
{
case InProgressTestNodeStateProperty _:
{
...
break;
}
case PassedTestNodeStateProperty _:
{
...
break;
}
case FailedTestNodeStateProperty failedTestNodeStateProperty:
{
...
break;
}
case SkippedTestNodeStateProperty _:
{
...
break;
}
...
}
return Task.CompletedTask;
}
...
}
最後,API 採用 CancellationToken,擴充功能應該遵循它。
這很重要
直接在 ConsumeAsync 方法中處理承載資料。 一般的 IDataConsumer 會以非同步方式取用資料:IMessageBus 會將每個已發佈的承載資料排入佇列,並在背景迴圈中處理,因此 IMessageBus.PublishAsync 不會阻塞生產者,而且無法保證 ConsumeAsync 會在生產者繼續執行其工作時的哪個時間點執行。 該平台序列化傳送,使每位消費者同時只處理一個有效載荷,免除了單一消費者內部複雜的同步需求。
備註
對於必須保證在生產者繼續執行之前就已發生消費的情境(例如在測試開始執行之前),MTP 2.3.0 引入了實驗性 IBlockingDataConsumer 標記介面(需要抑制 TPEXP 診斷)。 也實作了 IBlockingDataConsumer 的消費者會由訊息匯流排以 內嵌 方式叫用:呼叫會依序執行,PublishAsync 會封鎖,直到 ConsumeAsync 完成,且任何由 ConsumeAsync 擲出的例外都會回傳給發布該資料的生產者。 訊息匯流排會跳過將生產者的資料回傳給同一個生產者(同一個 UID),所以用自己的 UID 發佈是安全的。 然而,阻塞式消費者不得在 ConsumeAsync 內部發佈資料,因為若該資料在 不同的 生產者 UID 下被路由回自身,這種重入會導致死鎖。
警告
在複合擴充點中搭配使用 IDataConsumer 與 ITestSessionLifetimeHandler 時,務必忽略在執行 ITestSessionLifetimeHandler.OnTestSessionFinishingAsync 之後收到的任何資料。
OnTestSessionFinishingAsync 是處理累積資料並將新資訊傳輸到 IMessageBus 的最後機會,因此,超出此點消耗的任何資料將無法由擴充功能使用。
如果您的擴充功能需要密集初始化並且需要使用 async/await 模式,您可以參考 Async extension initialization and cleanup。 如果需要在擴充點之間共用狀態,可以參考 CompositeExtensionFactory<T> 區段。
訊息匯流排檔案產物
自訂 IData 有效載荷沒有自動的使用者介面或命令列輸出。 平台只會顯示有匹配消費者註冊的資料,所以如果你發佈自己的 IData 類型,但沒有人使用,就不會被列印或轉發。 若要讓擴充功能產生的檔案可供使用者和工具看見,請發佈其中一種內建的檔案成品訊息;內建的 terminal 和 dotnet test 取用者已可辨識這些訊息。
對於執行層級或工作階段層級的檔案,請發佈 FileArtifact 或 SessionFileArtifact。 兩者皆於 MTP 1.0.0 中引入,並存在於命名空間中 Microsoft.Testing.Platform.Extensions.Messages :
-
FileArtifact未被範圍限制。 將它用於不與特定測試工作階段關聯的檔案。 -
SessionFileArtifact的作用範圍透過其SessionUid限於一次執行/工作階段。 可用於整次執行所產生的產物,例如涵蓋率結果、報告、傾印檔或錄製的影片。
內建終端機與 dotnet test 使用者會擷取兩種類型並列印或轉發檔案路徑,使檔案在主控台輸出及管道 dotnet test 中都能被發現。 由於使用者控制最終呈現方式,請將每個檔案保留在磁碟上並維持可用,直到它不再可能被取用或轉寄;請勿在同一次 PublishAsync 呼叫中將其刪除。
工件物件本身不具備生產者身份。 訊息匯流排透過 IDataProducer 的dataProducer參數向每位消費者提供來源IDataConsumer.ConsumeAsync資訊,因此消費者從該參數中得知是誰產生了檔案,而非從產物本身。 訊息匯流排也不會擁有、移動或刪除該參考檔案:製作者擁有檔案的生命週期,消費者只能接收、讀取或轉發其路徑。
備註
第三方 IDataConsumer 註冊僅在 builder.TestHost 上公開(即 處理序內 測試主機)。 沒有供 處理序外 用戶使用的公開 builder.TestHostControllers.AddDataConsumer API。 會顯示成品的原生報告擴充功能使用平台內部的整合機制,而自訂擴充功能無法依賴這些機制。 若要顯示來自您自有擴充功能的檔案,請發佈這裡所述的內建成品訊息,並讓內建取用端將其呈現出來。
發佈會話產物的生產者必須實作 IDataProducer 並列出它在 中發布 DataTypesProduced的精確執行時訊息類型。 以下範例會在會話結束時發布覆蓋報告:
internal sealed class CoverageReportProducer(IMessageBus messageBus)
: IDataProducer, ITestSessionLifetimeHandler
{
public string Uid => nameof(CoverageReportProducer);
public string Version => "1.0.0";
public string DisplayName => "Coverage report producer";
public string Description => "Publishes the coverage report as a session artifact.";
// List the exact runtime message types this producer publishes.
public Type[] DataTypesProduced => new[] { typeof(SessionFileArtifact) };
public Task<bool> IsEnabledAsync() => Task.FromResult(true);
public Task OnTestSessionStartingAsync(ITestSessionContext context)
=> Task.CompletedTask;
public Task OnTestSessionFinishingAsync(ITestSessionContext context)
{
var report = new FileInfo("coverage.cobertura.xml");
return messageBus.PublishAsync(
this,
new SessionFileArtifact(
context.SessionUid,
report,
"Code coverage",
"Cobertura coverage report for the run."));
}
}
若要將檔案附加至 特定測試,讓終端機、dotnet test和 IDE 將該檔案與該測試建立關聯並顯示出來,請不要發佈獨立的檔案成品。 請改為將一或多個FileArtifactProperty項目新增至TestNode,而這些項目是你的測試框架透過TestNodeUpdateMessage回報的。
FileArtifactProperty 於 MTP 1.7.0 中引入:
var testNode = new TestNode
{
Uid = testUid,
DisplayName = testDisplayName,
Properties = new PropertyBag(
PassedTestNodeStateProperty.CachedInstance,
new FileArtifactProperty(
new FileInfo("screenshot.png"),
"Failure screenshot",
"Screenshot captured while the test ran.")),
};
await messageBus.PublishAsync(
dataProducer,
new TestNodeUpdateMessage(sessionUid, testNode));
這很重要
TestNodeFileArtifact 已過時,並於 MTP 2.0.0 中移除。 若要附加測試層級檔案,請在 TestNode 上使用 FileArtifactProperty。 如需詳細資訊,請參閱從 Microsoft.Testing.Platform (MTP) v1 移轉至 v2。
備註
MTP 2.4.0(截至 2026 年 7 月尚未發布)為 FileArtifact 和 SessionFileArtifact 新增了實驗性的 kind 建構器多載與 Kind 屬性(需要抑制 TPEXP 診斷)。
Kind是產出者主張的反向 DNS 識別碼,代表工件格式(例如 、 、 microsoft.testing.trxmicrosoft.testing.junit、 或microsoft.testing.ctrf),microsoft.testing.html後處理可用來將相同格式的工件分組以便整合。 如果提供者未宣告已知的種類,請保留為 null 或將其省略。 目前, Kind 這只是一份元資料合約;不要假設會有更廣泛的合併協調。
#pragma warning disable TPEXP // Experimental API.
new SessionFileArtifact(
context.SessionUid,
trxFile,
"TRX report",
"Test results in TRX format.",
kind: "microsoft.testing.trx");
#pragma warning restore TPEXP
ITestHostEnvironmentVariableProvider 擴充功能
ITestHostEnvironmentVariableProvider 是處理序外擴充功能,使您能夠為測試主機建立自訂環境變數。 利用此擴充點可確保測試平台將啟動具有適當環境變數的新主機,如架構區段中詳述。
若要註冊自訂 ITestHostEnvironmentVariableProvider,請使用下列 API:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHostControllers.AddEnvironmentVariableProvider(
static serviceProvider => new CustomEnvironmentVariableForTestHost());
工廠利用 IServiceProvider 來存取測試平台提供的服務套件。
這很重要
註冊順序很重要,因為 API 是按照註冊順序呼叫的。
ITestHostEnvironmentVariableProvider 介面包含以下方法和類型:
public interface ITestHostEnvironmentVariableProvider : ITestHostControllersExtension, IExtension
{
Task UpdateAsync(IEnvironmentVariables environmentVariables);
Task<ValidationResult> ValidateTestHostEnvironmentVariablesAsync(
IReadOnlyEnvironmentVariables environmentVariables);
}
public interface IEnvironmentVariables : IReadOnlyEnvironmentVariables
{
void SetVariable(EnvironmentVariable environmentVariable);
void RemoveVariable(string variable);
}
public interface IReadOnlyEnvironmentVariables
{
bool TryGetVariable(
string variable,
[NotNullWhen(true)] out OwnedEnvironmentVariable? environmentVariable);
}
public sealed class OwnedEnvironmentVariable : EnvironmentVariable
{
public IExtension Owner { get; }
public OwnedEnvironmentVariable(
IExtension owner,
string variable,
string? value,
bool isSecret,
bool isLocked);
}
public class EnvironmentVariable
{
public string Variable { get; }
public string? Value { get; }
public bool IsSecret { get; }
public bool IsLocked { get; }
}
ITestHostEnvironmentVariableProvider 是 ITestHostControllersExtension 的一種,可作為所有測試主機控制器擴充功能的基礎。 與所有其他擴充點一樣,它也繼承自 IExtension。 因此,與任何其他擴充功能一樣,您可以選擇使用 IExtension.IsEnabledAsync API 啟用或停用它。
考慮此 API 的詳細資訊:
UpdateAsync:此更新 API 提供了 IEnvironmentVariables 物件的執行個體,您可以從中呼叫 SetVariable 或 RemoveVariable 方法。 使用 SetVariable 時,必須傳遞類型為 EnvironmentVariable 的物件,該物件需要以下規範:
-
Variable:環境變數的名稱。 -
Value:環境變數的值。 -
IsSecret:這指示環境變數是否包含不應記錄,或透過TryGetVariable存取的敏感資訊。 -
IsLocked:決定其他ITestHostEnvironmentVariableProvider擴充功能是否可以修改該值。
ValidateTestHostEnvironmentVariablesAsync:該方法是在所有已註冊的 UpdateAsync 執行個體之 ITestHostEnvironmentVariableProvider 方法被呼叫後才被呼叫。 它可讓您驗證環境變數的設定是否正確。 它需要實作 IReadOnlyEnvironmentVariables 的物件,該物件提供了使用 TryGetVariable 物件類型獲取特定環境變數資訊的 OwnedEnvironmentVariable 方法。 驗證後,您將傳回包含任何失敗原因的 ValidationResult。
備註
測試平台預設實作並註冊 SystemEnvironmentVariableProvider。 此提供者會載入所有目前環境變數。 作為第一個註冊的提供者,它先行執行,為所有其他 ITestHostEnvironmentVariableProvider 使用者擴充功能提供存取預設環境變數的權限。
如果您的擴充功能需要密集初始化並且需要使用 async/await 模式,您可以參考 Async extension initialization and cleanup。 如果需要在擴充點之間共用狀態,可以參考 CompositeExtensionFactory<T> 區段。
ITestHostProcessLifetimeHandler 擴充功能
ITestHostProcessLifetimeHandler 是處理序外擴充功能,可讓您從外部角度觀察測試主機處理序。 這可確保您的擴充功能不受測試程式碼可能造成的潛在當機或停止回應的影響。 使用此擴充點將提示測試平台啟動新主機,如架構區段詳述。
若要註冊自訂 ITestHostProcessLifetimeHandler,請使用下列 API:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHostControllers.AddProcessLifetimeHandler(
static serviceProvider => new CustomMonitorTestHost());
工廠利用 IServiceProvider 來存取測試平台提供的服務套件。
這很重要
註冊順序很重要,因為 API 是按照註冊順序呼叫的。
ITestHostProcessLifetimeHandler 介面包含以下方法:
public interface ITestHostProcessLifetimeHandler : ITestHostControllersExtension
{
Task BeforeTestHostProcessStartAsync(CancellationToken cancellationToken);
Task OnTestHostProcessStartedAsync(
ITestHostProcessInformation testHostProcessInformation,
CancellationToken cancellation);
Task OnTestHostProcessExitedAsync(
ITestHostProcessInformation testHostProcessInformation,
CancellationToken cancellation);
}
public interface ITestHostProcessInformation
{
int PID { get; }
int ExitCode { get; }
bool HasExitedGracefully { get; }
}
ITestHostProcessLifetimeHandler 是 ITestHostControllersExtension 的一種,可作為所有測試主機控制器擴充功能的基礎。 與所有其他擴充點一樣,它也繼承自 IExtension。 因此,與任何其他擴充功能一樣,您可以選擇使用 IExtension.IsEnabledAsync API 啟用或停用它。
請考慮此 API 的以下詳細資訊:
BeforeTestHostProcessStartAsync:此方法在測試平台啟動測試主機之前呼叫。
OnTestHostProcessStartedAsync:測試主機啟動後立即呼叫此方法。 此方法提供一個實作 ITestHostProcessInformation 介面的物件,該介面會提供有關測試主機處理序結果的關鍵詳細資訊。
這很重要
呼叫此方法不會停止測試主機的執行。 如果您需要暫停它,您應該註冊一個處理序內擴充功能,例如 ITestHostApplicationLifetime,並將其與處理序外擴充功能同步。
OnTestHostProcessExitedAsync:當測試套件執行完成時呼叫此方法。 此方法提供了一個符合 ITestHostProcessInformation 介面的物件,該物件傳達有關測試主機處理序結果的重要細節。
ITestHostProcessInformation 介面提供下列詳細資訊:
-
PID:測試主機的處理序識別碼。 -
ExitCode:處理序的結束程式碼。 該值僅在OnTestHostProcessExitedAsync方法內可用。 嘗試在OnTestHostProcessStartedAsync方法內存取它將會導致例外狀況。 -
HasExitedGracefully:一個布林值,指示測試主機是否當機。 如果為 True,則表示測試主機沒有正常地退出。
使用 TestingPlatformBuilderHook 自動註冊你的擴充功能
每個前面的擴充功能區段都會顯示 手動註冊呼叫(例如 builder.TestHost.AddDataConsumer(...))。 要求消費者編輯他們的 Main 方法,是一種糟糕的入門體驗。
Microsoft.Testing.Platform.MSBuild 套件透過產生一個從自動產生的入口點執行的 SelfRegisteredExtensions.AddSelfRegisteredExtensions(builder, args) 方法來解決這個問題。 要將你的擴充功能插入該產生的方法,請在你的 NuGet 套件中傳送兩個產物:
- 一個公開的靜態
TestingPlatformBuilderHook類別,裡面有一個AddExtensions方法會註冊你的擴充功能。 - 一個 MSBuild props 檔案,宣告一個指向該類別的
<TestingPlatformBuilderHook>項目。
當使用者安裝你的套件時,MSBuild 整合會辨識到該項目,並產生對你的 Hook 的呼叫;如此一來,無需在使用端變更任何程式碼,你的擴充功能就會完成註冊。
備註
只有當使用者的專案中包含 Microsoft.Testing.Platform.MSBuild(MSTest、NUnit 和 xUnit 執行器都會透過相依性鏈結間接包含它),且未透過設定 <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint> 停用此功能時,自動註冊才會運作。 使用者即使停用自動產生的入口點,仍需從他們的 Main 方法呼叫你的手動註冊 API。
建立 hook 類別
在您的擴充功能組件中加入一個 public static class TestingPlatformBuilderHook,其中包含一個 AddExtensions(ITestApplicationBuilder, string[]) 方法,用來執行原本使用者必須手動呼叫的相同註冊作業:
using Microsoft.Testing.Platform.Builder;
namespace Contoso.MyExtension;
public static class TestingPlatformBuilderHook
{
public static void AddExtensions(ITestApplicationBuilder testApplicationBuilder, string[] arguments)
=> testApplicationBuilder.AddMyExtension();
}
類別名稱不一定要是 TestingPlatformBuilderHook——MSBuild 項目會以完整型別名稱指向它——但使用這個名稱能讓你的程式碼與盒內的擴充功能如 Microsoft.Testing.Extensions.Retry 和 Microsoft.Testing.Extensions.HotReload 保持一致。
該方法必須:
- 成為
public static。 - 第一個參數類型為
Microsoft.Testing.Platform.Builder.ITestApplicationBuilder。 - 具有第二個類型為
string[]的參數(傳遞給測試主機的命令列引數)。 如果你的擴充功能不需要它,就可以忽略。 - 返回
void。
宣告 MSBuild 項目
在您的 NuGet 套件中,於 buildMultiTargeting/<PackageId>.props 底下隨附一個 props 檔案。 宣告一個 <TestingPlatformBuilderHook> 項目,將 MSBuild 任務指向你的 hook 類別:
<Project>
<ItemGroup>
<TestingPlatformBuilderHook Include="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
<DisplayName>Contoso.MyExtension</DisplayName>
<TypeFullName>Contoso.MyExtension.TestingPlatformBuilderHook</TypeFullName>
</TestingPlatformBuilderHook>
</ItemGroup>
</Project>
元資料如下:
-
Include:可唯一識別您的掛鉤的 GUID。 請參考 GUIDInclude是一個隨機識別碼。 -
DisplayName:MSBuild 診斷訊息中在產生入口點時顯示的友善名稱。 請使用您的套件或擴充功能名稱。 -
TypeFullName:你之前建立的類別的完整限定名稱TestingPlatformBuilderHook。 MSBuild 任務使用此項將global::Contoso.MyExtension.TestingPlatformBuilderHook.AddExtensions(builder, args);輸出至產生的進入點。
Include GUID 是一個隨機識別碼
Include 屬性中的 GUID 不 與你擴充套件的 IExtension.Uid相同。 它是一個註冊識別碼,供 MSBuild 工作用來去除 NuGet 參考之間重複的掛鉤,並在少數幾種已知情況下決定其順序。
當你建立新的擴充功能時,請產生一個全新的 GUID,並將該 GUID 硬式編碼到你的 props 檔案中。 產生一個的方法:
- PowerShell:
[guid]::NewGuid() - Visual Studio:Tools>Create GUID
-
uuidgen在 Linux 和 macOS 上
這很重要
切勿從其他擴充功能的 props 檔案中複製 GUID,無論該擴充功能是由 Microsoft 或第三方提供。 具有相同 Include 值的兩個擴充功能會被視為重複項目:只會呼叫其中一個掛鉤,因此你的擴充功能將會在無提示的情況下註冊失敗。
備註
一旦你發布了 GUID,就應將其視為永久不變。 在後續版本中變更它本身並沒有害處,但在未來的套件版本中,若將 舊值重複用於不同的鉤子,可能會讓其相依性圖中同時包含這兩個版本的使用者在升級期間產生混淆。
確認掛鉤有沒有接線
在使用 Microsoft.Testing.Platform.MSBuild 的測試專案中安裝套件後,建置專案,並檢查位於 SelfRegisteredExtensions.g.cs 下產生的 obj/<Configuration>/<TargetFramework>/ 檔案。 你應該會看到一個叫入鉤球的指令,例如:
public static void AddSelfRegisteredExtensions(this global::Microsoft.Testing.Platform.Builder.ITestApplicationBuilder builder, string[] args)
{
global::Contoso.MyExtension.TestingPlatformBuilderHook.AddExtensions(builder, args);
}
如果沒有該呼叫,請再次確認 props 檔案是否已封裝在 buildMultiTargeting/ 內的 build/ 之下(不是 .nupkg),且 DisplayName 與 TypeFullName 中繼資料存在,並確認取用端尚未設定 <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>。
擴充功能執行順序
測試平台由測試框架和任意數量的擴充功能組成,這些擴充功能可以在處理序內或處理序外執行。 本文件概述了對所有潛在擴充點的呼叫順序,以清楚地說明預計何時叫用某個功能:
- ITestHostEnvironmentVariableProvider.UpdateAsync:處理序外
- ITestHostEnvironmentVariableProvider.ValidateTestHostEnvironmentVariablesAsync:處理序外
- ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync:處理序外
- 測試主機程序啟動
- ITestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync:在處理序外,此事件可以根據競爭條件將處理序內擴充功能的操作交織在一起。
- ITestHostApplicationLifetime.BeforeRunAsync:正在進行中
- ITestSessionLifetimeHandler.OnTestSessionStartingAsync:處理中
- ITestFramework.CreateTestSessionAsync:進程內
- ITestFramework.ExecuteRequestAsync:在處理序內,可以呼叫此方法一次或多次。 此時,測試框架將向 IDataConsumer 可以使用的 IMessageBus 傳輸訊息。
- ITestFramework.CloseTestSessionAsync:處於處理中
- ITestSessionLifetimeHandler.OnTestSessionFinishingAsync:進程內
- ITestHostApplicationLifetime.AfterRunAsync:進行中
- 處理序內清理涉及在所有擴充點上呼叫 dispose 和 IAsyncCleanableExtension。
- ITestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync:處理序外
- 處理序外清理涉及在所有擴充點上呼叫 dispose 和 IAsyncCleanableExtension。
擴充功能輔助工具
測試平台提供了一組輔助類別和介面來簡化擴充功能的實作。 這些輔助工具的設計目的是簡化開發流程,並確保擴充功能遵循平台的標準。
擴充功能的非同步初始化和清理
透過工廠建立測試框架及擴充功能,遵循標準的 .NET 物件建立機制,該機制使用同步建構器。 如果擴充功能需要密集的初始化 (例如存取檔案系統或網路),則它不能在建構函式中使用 async/await 模式,因為建構函式會傳回無效,而不是 Task。
因此,測試平台提供了一種透過簡單的介面使用非同步/等待模式初始化擴充功能的方法。 為了保持對稱,它還提供了一個非同步介面以進行清理,讓擴充功能能夠順利地加以實作。
public interface IAsyncInitializableExtension
{
Task InitializeAsync();
}
public interface IAsyncCleanableExtension
{
Task CleanupAsync();
}
IAsyncInitializableExtension.InitializeAsync:這個方法保證會在創建工廠之後叫用。
IAsyncCleanableExtension.CleanupAsync:此方法在測試工作階段終止期間,確保至少被執行一次,並在預設或DisposeAsync之前進行。
這很重要
與標準 Dispose 方法類似,CleanupAsync 可以多次叫用。 如果多次呼叫物件的 CleanupAsync 方法,則該物件必須忽略第一次呼叫之後的所有呼叫。 如果多次呼叫該物件的 CleanupAsync 方法,則該物件不得擲回例外狀況。
備註
預設情況下,測試平台會呼叫 DisposeAsync(如果它是可用的),否則會呼叫 Dispose(如果它已實作)。 需要注意的是,測試平台不會同時呼叫兩種釋放方法,而是會優先使用非同步的方法(如果已經實作)。
CompositeExtensionFactory<T>
如擴充功能區段所述,測試平台使您能夠實作介面以在處理序內和處理序外合併自訂擴充功能。
每個介面針對特定功能,根據 .NET 設計,你會在特定物件中實作這個介面。 您可以使用 AddXXX 中的特定註冊 API TestHost 或 TestHostController 中的 ITestApplicationBuilder 物件來註冊擴充功能本身,如相應區段中詳述。
但是,如果您需要在兩個擴充功能之間共用狀態,由於您可以實作和註冊不同介面所需的不同物件,這使得共用成為一項具有挑戰性的任務。 如果沒有任何幫助,您將需要一種將一個擴充功能傳遞給另一個擴充功能以共用資訊的方法,這使得設計變得複雜。
因此,測試平台提供了一種複雜的方法來使用相同類型實作多個擴充點,使資料共用成為一項簡單的工作。 您需要做的就是使用 CompositeExtensionFactory<T>,然後可以使用與單一介面實現相同的 API 進行註冊。
例如,考慮一個同時實作 ITestSessionLifetimeHandler 和 IDataConsumer 的類型。 這是一種常見的情境,因為您通常會想要從測試框架收集資訊,然後在測試工作階段結束時,您將使用 IMessageBus 中的 ITestSessionLifetimeHandler.OnTestSessionFinishingAsync 來分派您的成品。
您應該做的是正常實作介面:
internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ...
{
...
}
一旦為您的類型建立了 CompositeExtensionFactory<CustomExtension>,您就可以使用 IDataConsumer 和 ITestSessionLifetimeHandler API 來註冊它,這為 CompositeExtensionFactory<T> 提供了重載:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
var factory = new CompositeExtensionFactory<CustomExtension>(serviceProvider => new CustomExtension());
builder.TestHost.AddTestSessionLifetimeHandle(factory);
builder.TestHost.AddDataConsumer(factory);
處理站建構函式使用 IServiceProvider 來存取測試平台提供的服務。
測試平台將負責管理組合擴充功能的生命週期。
需要注意的是,由於測試平台同時支援處理序內和處理序外擴充功能,因此您不能任意組合任何擴充點。 擴充功能的建立和使用取決於主機類型,這代表您只能將處理序內 (TestHost) 和處理序外 (TestHostController) 擴充功能分組在一起。
可能有以下組合:
- 對於
ITestApplicationBuilder.TestHost,您可以組合IDataConsumer和ITestSessionLifetimeHandler。 - 對於
ITestApplicationBuilder.TestHostControllers,您可以組合ITestHostEnvironmentVariableProvider和ITestHostProcessLifetimeHandler。
備註
IDataConsumer 是 程序內 擴充功能,因此自訂取用者只能透過 builder.TestHost 註冊(包括經由 CompositeExtensionFactory<T>)。 沒有公開的 API 可在 builder.TestHostControllers 上註冊 IDataConsumer。