本文說明如何為 Microsoft.Testing.Platform 建立自訂測試框架。 測試框架是唯一強制性的擴充功能。 它會發現並執行測試,並將結果回報給平台。
關於完整的擴充點摘要及進程/出程概念,請參見「建立自訂擴充」。
如果你正在遷移現有的 VSTest 測試框架,建議原生實作介面。 VSTest 橋接擴充功能作為過渡步驟可用,但原生實作能提供最佳體驗。
測試框架擴展
測試框架是為測試平台提供發現和執行測試能力的主要擴充功能。 測試框架負責將測試結果傳達回測試平台。 測試框架是執行測試工作階段所需的唯一強制擴充功能。
註冊測試框架
本節介紹如何將測試框架註冊到測試平台。 每個測試應用程式產生器只能使用 API 註冊一個測試框架,如 Microsoft.Testing.Platform 架構 檔案所示。
註冊 API 定義如下:
ITestApplicationBuilder RegisterTestFramework(
Func<IServiceProvider, ITestFrameworkCapabilities> capabilitiesFactory,
Func<ITestFrameworkCapabilities, IServiceProvider, ITestFramework> adapterFactory);
API 需要兩個處理站:
:這是一個委派,它接受實作 介面的物件,並傳回實作 介面的物件。 可讓您存取平台服務,例如組態、記錄器和命令列參數。
介面用於向平台和擴充功能宣告測試框架支援的功能。 它允許平台和擴充功能透過實作和支援特定行為來正確互動。 為了更理解功能的概念,請參閱相應的章節。
:這是一個委派,它接受 ITestFrameworkCapability 物件 (該物件是由 和 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();
備註
傳回空的 ITestFrameworkCapability 不應阻止測試工作階段的執行。 所有測試框架都應該能夠發現和執行測試。 如果測試框架缺乏某些功能,影響應僅限於可能選擇不參加的擴充功能。
建立測試框架
是透過提供測試框架的擴充功能來實作的:
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();
}
:這是任何類型請求的基本介面。 您應該將測試框架視為處理序內有狀態伺服器,其生命週期為:
表示測試框架生命週期的序列圖。
上圖說明了測試平台在建立測試框架執行個體後發出了三個請求。 測試框架處理這些請求,並利用請求本身中包含的 服務來交付每個特定請求的結果。 一旦處理了特定請求,測試框架就會呼叫其上的 方法,向測試平台指示該請求已滿足。 測試平台監控所有傳送的請求。 一旦滿足所有請求,它就會叫用 並處置實例(如果已實作 )。 顯然,請求及其完成可以重疊,從而實作請求的並行和非同步執行。
備註
目前,測試平台不會傳送重疊的請求,而是等待請求 完成後再傳送下一個請求。 但是,這種行為將來可能會改變。 對並行請求的支援將透過功能系統來確定。
實作指定了需要滿足的精確請求。 測試框架識別請求的類型並相應地處理它。 如果請求類型無法識別,則應引發例外狀況。
您可以在 IRequest 區段中找到有關可用請求的詳細資訊。
:該服務與請求連結,允許測試框架非同步地將有關正在進行的請求的資訊發佈到測試平台。 訊息總線充當平台的中心樞紐,促進所有平台元件和擴充功能之間的非同步通訊。 有關可以發佈到測試平台的資訊的完整清單,請參閱 IMessageBus 一節。
:該令牌用於中斷處理特定請求。
:如前面的序列所示, 方法會通知平台請求已成功處理,且所有相關資訊已傳輸至 IMessageBus。
警告
忽略對請求叫用 將導致測試應用程式變得無回應。
若要根據您或使用者的要求自訂測試框架,您可以使用設定檔中的個人化區段,或使用自訂命令列選項。
處理請求
後續各節詳細描述了測試框架可能接收和處理的各種請求。
在繼續下一節之前,徹底理解 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
{
}
: 由兩個屬性組成: 和 。 表示測試可能有父測試,引入了測試樹狀結構的概念,其中 可以相互關聯排列。 此結構可根據節點之間的樹狀結構關係,進行增強和功能擴充。 如果您的測試框架不需要測試樹狀結構,您可以選擇不使用它,只需將其設為 Null,就能獲得簡單的 平面清單。
: 由三個屬性組成,其中之一是 類型的 。 做為節點的唯一穩定識別碼。 術語「唯一穩定識別碼」代表相同的 應該在不同的執行和作業系統之間保持相同的。 是測試平台原樣接受的任意不透明字串。
這很重要
識別碼的穩定性和唯一性在測試領域至關重要。 它們能夠精確鎖定單一測試以進行執行,並允許測試 ID 作為永久識別碼,從而促進強大的擴充和功能。
第二個屬性是 ,這是測試的人性化名稱。 例如,執行 命令列時會顯示該名稱。
第三個屬性是 ,它是一種 類型。 如程式碼所示,這是一個專門的屬性包,包含 的通用屬性。 這表示您可以將任何屬性附加到實作預留位置介面 的節點。
測試平台識別新增到 的特定屬性,以確定測試是否通過、失敗或略過。
您可以在 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; }
}
用於傳遞有關 執行的計時詳細資訊。 它還可透過 來計時各個執行步驟。 當您的測試概念分為多個階段 (例如初始化、執行和清理) 時,這特別有用。
以下屬性中必須指定其中一個,並將結果傳達給測試平台。
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; }
}
會通知測試平台此 因取消而失敗。