为 Microsoft.Testing.Platform (MTP) 构建扩展组件。

本文介绍 MTP 超出测试框架本身的扩展点。 有关测试框架的创建,请参阅 生成测试框架

有关完整的扩展点摘要和进程内/进程外概念,请参阅 “创建自定义扩展”。

扩展点

测试平台提供额外的可扩展性点,让你可以自定义平台和测试框架的行为。 这些扩展性点是可选的,可用于增强测试体验。

Tip

本文中显示的每个扩展都包含一个手动注册代码段(例如, builder.TestHost.AddDataConsumer(...))。 如果你以 NuGet 包的形式发布扩展,可以通过公开一个 TestingPlatformBuilderHook 和一个小的 MSBuild props 文件,让使用者跳过手动调用。 然后,自动生成的入口点会自动调用你的钩子。 有关详情,请参阅使用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());

在所提供的示例中,CustomCommandLineOptionsICommandLineOptionsProvider 接口的实现,该接口包括以下成员和数据类型:

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 的执行顺序是:

一个表示“ICommandLineOptionsProvider”接口执行顺序的示意图。

让我们来检查 API 及其平均值:

ICommandLineOptionsProvider.GetCommandLineOptions():此方法用于检索组件提供的所有选项。 每个 CommandLineOption 都需要指定以下属性:

string name:这是不含破折号的选项名称。 例如,“筛选器”将被用户用作 --filter

string description:这是选项的说明。 当用户将 --help 作为参数传递给应用程序生成器时,它将显示出来。

ArgumentArity arity:选项的实参数量是指如果指定了该选项或命令,可以传递的值的个数。 当前的可用实参数量包括:

  • Zero:表示参数的元数为零。
  • ZeroOrOne:表示参数元数为零或一。
  • ZeroOrMore:表示参数的元数为零或更多。
  • OneOrMore:表示一个或多个参数元数。
  • ExactlyOne:表示正好一个参数实参数量。

有关示例,请参阅 System.CommandLine arity 表

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(
        SessionUid sessionUid,
        CancellationToken cancellationToken);

    Task OnTestSessionFinishingAsync(
        SessionUid sessionUid,
        CancellationToken cancellationToken);
}

public readonly struct SessionUid(string value)
{
    public string Value { get; } = value;
}

public interface ITestHostExtension : IExtension
{
}

ITestSessionLifetimeHandlerITestHostExtension 的一种类型,是所有“测试主机”扩展的基础。 与所有其他扩展点一样,它也继承自 IExtension。 因此,与其他扩展一样,可以使用 IExtension.IsEnabledAsync API 来选择将其启用或禁用。

请考虑此 API 的以下详细信息:

OnTestSessionStartingAsync:在方法会在测试会话开始前调用,并接收 SessionUid 对象,该对象提供了当前测试会话的不透明标识符。

OnTestSessionFinishingAsync:测试会话完成后会调用此方法,确保测试框架已完成所有测试的执行,并向平台报告了所有相关数据。 在这种方法中,扩展通常使用 IMessageBus 将自定义资产或数据传输到共享平台总线。 此方法还可以向任何自定义“进程外”扩展发出测试会话已结束的信号。

最后,这两个 API 都会使用扩展也应遵循的 CancellationToken

如果扩展需要密集初始化,并且需要使用 async/await 模式,则可以参考 Async extension initialization and cleanup。 如果需要在扩展点之间“共享状态”,则可以参考 CompositeExtensionFactory<T> 部分。

ITestApplicationLifecycleCallbacks 扩展程序

ITestApplicationLifecycleCallbacks 是“进程内”扩展,可以在所有内容之前执行代码,就像可以访问“测试主机”的假设“主要”的第一行。

要注册自定义 ITestApplicationLifecycleCallbacks,请使用以下 API:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHost.AddTestApplicationLifecycleCallbacks(
    static serviceProvider
    => new CustomTestApplicationLifecycleCallbacks());

工厂利用 IServiceProvider 来访问测试平台提供的服务套件。

重要

注册的顺序非常重要,因为 API 是按照注册的顺序调用的。

ITestApplicationLifecycleCallbacks 接口包括以下方法:

public interface ITestApplicationLifecycleCallbacks : ITestHostExtension
{
    Task BeforeRunAsync(CancellationToken cancellationToken);

    Task AfterRunAsync(
        int exitCode,
        CancellationToken cancellation);
}

public interface ITestHostExtension : IExtension
{
}

ITestApplicationLifecycleCallbacksITestHostExtension 的一种类型,是所有“测试主机”扩展的基础。 与所有其他扩展点一样,它也继承自 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 : ITestHostExtension
{
    Type[] DataTypesConsumed { get; }

    Task ConsumeAsync(
        IDataProducer dataProducer,
        IData value,
        CancellationToken cancellationToken);
}

public interface IData
{
    string DisplayName { get; }
    string? Description { get; }
}

IDataConsumerITestHostExtension 的一种类型,是所有“测试主机”扩展的基础。 与所有其他扩展点一样,它也继承自 IExtension。 因此,与其他扩展一样,可以使用 IExtension.IsEnabledAsync API 来选择将其启用或禁用。

DataTypesConsumed:此属性会返回扩展计划使用的 Type 列表。 它对应于 IDataProducer.DataTypesProduced。 值得注意的是,IDataConsumer 可以订阅来自不同 IDataProducer 实例的多个类型,而不会出现任何问题。

ConsumeAsync:每当当前使用者订阅的数据类型被推送到 IMessageBus 时,就会触发此方法。 它会接收 IDataProducer,以便提供数据有效负载生成者以及 IData 有效负载本身的详细信息。 如你所见,IData 是一个包含一般信息数据的通用占位符接口。 能够推送不同类型 IData 意味着使用者需要“开启”类型本身,将其转换为正确的类型并访问特定信息。

如果使用者希望详细说明由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 方法中直接处理有效负载至关重要。 IMessageBus 可以管理同步和异步处理,并与测试框架协调执行。 虽然在撰写本文档时使用过程完全是异步的,并且不会阻塞 IMessageBus.Push,但这只是实现的详细信息,将来可能会根据未来的需求而发生变化。 但是,平台可确保该方法始终被调用一次,从而消除了复杂的同步需求,并可管理使用者的可伸缩性。

警告

IDataConsumer中结合 ITestHostProcessLifetimeHandler 使用 时,必须忽略执行 ITestSessionLifetimeHandler.OnTestSessionFinishingAsync 后收到的任何数据OnTestSessionFinishingAsync 是处理累积数据并向 IMessageBus 传输新信息的最后机会,因此,超出这一点的任何数据将无法供扩展“使用”

如果扩展需要密集初始化,并且需要使用 async/await 模式,则可以参考 Async extension initialization and cleanup。 如果需要在扩展点之间“共享状态”,则可以参考 CompositeExtensionFactory<T> 部分。

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; }
}

ITestHostEnvironmentVariableProviderITestHostControllersExtension 的一种类型,是所有“测试主机控制器”扩展的基础。 与所有其他扩展点一样,它也继承自 IExtension。 因此,与其他扩展一样,可以使用 IExtension.IsEnabledAsync API 来选择将其启用或禁用。

请考虑此 API 的详细信息:

UpdateAsync:此更新 API 提供了 IEnvironmentVariables 对象的实例,可以从中调用 SetVariableRemoveVariable 方法。 在使用 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; }
}

ITestHostProcessLifetimeHandlerITestHostControllersExtension 的一种类型,是所有“测试主机控制器”扩展的基础。 与所有其他扩展点一样,它也继承自 IExtension。 因此,与其他扩展一样,可以使用 IExtension.IsEnabledAsync API 来选择将其启用或禁用。

请考虑此 API 的以下详细信息:

BeforeTestHostProcessStartAsync:此方法会在测试平台启动测试主机之前调用。

OnTestHostProcessStartedAsync:此方法会在测试主机启动后立即调用。 该方法提供了一个实现 ITestHostProcessInformation 接口的对象,该对象提供了测试主机进程结果的关键详细信息。

重要

调用此方法不会停止测试主机的执行。 如果需要暂停,应注册进程内扩展,如 ITestApplicationLifecycleCallbacks,并与“进程外”扩展同步。

OnTestHostProcessExitedAsync:此方法会在测试套件执行完成后调用。 此方法提供了一个符合 ITestHostProcessInformation 接口的对象,该对象传达了有关测试主机进程结果的关键详细信息。

ITestHostProcessInformation 接口提供以下详细信息:

  • PID:测试主机的进程 ID。
  • ExitCode:进程的退出代码。 此值只能在 OnTestHostProcessExitedAsync 方法中使用。 尝试在 OnTestHostProcessStartedAsync 方法中访问它将导致异常。
  • HasExitedGracefully:一个布尔值,表示测试主机是否已崩溃。 如果为 true,则表示测试主机没有正常退出。

使用 TestingPlatformBuilderHook 自动注册您的扩展程序

前面的每个扩展部分都显示 手动 注册调用(例如, builder.TestHost.AddDataConsumer(...))。 要求使用者编辑其 Main 方法是一种糟糕的入职体验。 Microsoft.Testing.Platform.MSBuild 包通过生成从自动生成的入口点运行的 SelfRegisteredExtensions.AddSelfRegisteredExtensions(builder, args) 方法解决此问题。 要将你的扩展插入到生成的该方法中,请在 NuGet 包中包含两个项目:

  • 包含用于注册你的扩展的 AddExtensions 方法的公共静态 TestingPlatformBuilderHook 类。
  • 一个 MSBuild props 文件,其中声明了一个指向该类的 <TestingPlatformBuilderHook> 项。

当使用者安装你的包时,MSBuild 集成会识别该项并生成对你的挂钩的调用,这样你的扩展就会在无需使用者更改代码的情况下完成注册。

注释

只有当使用者的项目中包含 Microsoft.Testing.Platform.MSBuild(由 MSTest、NUnit 和 xUnit 运行器以传递方式包含)且未通过设置 <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint> 选择退出时,自动注册才会生效。 禁用自动生成入口点的使用者仍需要从其 Main 方法调用手动注册 API。

创建挂钩类

在你的扩展程序集中添加一个 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.RetryMicrosoft.Testing.Extensions.HotReload)保持一致。

该方法必须:

  • 应为 public static
  • 具有类型为 Microsoft.Testing.Platform.Builder.ITestApplicationBuilder 的第一个参数。
  • 具有第二个类型 string[] 参数(传递给测试主机的命令行参数)。 如果扩展不需要它,则可以忽略它。
  • 返回 void

声明 MSBuild 项

在 NuGet 包的 buildMultiTargeting/<PackageId>.props 下提供一个 props 文件。 声明一个 <TestingPlatformBuilderHook> 项,将 MSBuild 任务指向你的挂钩类:

<Project>
  <ItemGroup>
    <TestingPlatformBuilderHook Include="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
      <DisplayName>Contoso.MyExtension</DisplayName>
      <TypeFullName>Contoso.MyExtension.TestingPlatformBuilderHook</TypeFullName>
    </TestingPlatformBuilderHook>
  </ItemGroup>
</Project>

元数据如下所示:

  • Include:用于唯一标识您的挂钩的 GUID。 请参阅 GUID Include 是随机标识符
  • DisplayName:生成入口点时 MSBuild 诊断消息中显示的友好名称。 使用您的包或扩展名称。
  • TypeFullName:你之前创建的 TestingPlatformBuilderHook 类的完全限定名称。 MSBuild 任务使用此项在生成的入口点中输出 global::Contoso.MyExtension.TestingPlatformBuilderHook.AddExtensions(builder, args);

Include GUID 是随机标识符

Include 特性中的 GUID 等同于你扩展的 IExtension.Uid。 它是 MSBuild 任务用于对 NuGet 引用中的挂钩去重(并在少数已知情况下对它们进行排序)的注册标识符。

创作新扩展时,请在 props 文件中生成全新的 GUID 并对其进行硬编码。 生成一个的方法有几种:

  • PowerShell: [guid]::NewGuid()
  • Visual Studio:Tools>Create GUID
  • Linux 和 macOS 上的 uuidgen

重要

切勿从其他扩展的 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 文件是否已打包在 .nupkg 内的 buildMultiTargeting/ 下(而不是 build/ 下),是否存在 DisplayNameTypeFullName 元数据,以及使用方是否未设置 <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>

扩展执行顺序

测试平台由测试框架和任意数量的扩展组成,这些扩展可在进程内进程外运行。 本文档概述了所有潜在扩展点的“调用顺序”,以便明确预计何时会调用某项功能:

  1. ITestHostEnvironmentVariableProvider.UpdateAsync:进程外
  2. ITestHostEnvironmentVariableProvider.ValidateTestHostEnvironmentVariablesAsync:进程外
  3. ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync:进程外
  4. 测试主机进程启动
  5. ITestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync:在进程外,此事件可能会与进程内扩展的操作交织,具体取决于竞争条件。
  6. ITestApplicationLifecycleCallbacks.BeforeRunAsync:进程内
  7. ITestSessionLifetimeHandler.OnTestSessionStartingAsync。正在进行中
  8. ITestFramework.CreateTestSessionAsync:进程内
  9. ITestFramework.ExecuteRequestAsync:进程内,此方法可被调用一次或多次。 此时,测试框架将向 IMessageBus 传输信息,以供 IDataConsumer 使用。
  10. ITestFramework.CloseTestSessionAsync:进程内
  11. ITestSessionLifetimeHandler.OnTestSessionFinishingAsync:处理中
  12. ITestApplicationLifecycleCallbacks.AfterRunAsync:进程内
  13. 进程内清理涉及调用所有扩展点上的处置和 IAsyncCleanableExtension
  14. ITestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync:进程外
  15. 进程外清理涉及调用所有扩展点上的处置和 IAsyncCleanableExtension

扩展辅助工具

测试平台提供了一系列帮助程序类和接口,以简化扩展的实现。 这些帮助程序旨在简化开发流程,并确保扩展符合平台标准。

扩展的异步初始化和清理

通过工厂创建测试框架和扩展遵循使用同步构造函数的标准.NET对象创建机制。 如果扩展需要密集初始化(如访问文件系统或网络),则不能在构造函数中使用 async/await 模式,因为构造函数会返回 void,而不是 Task

因此,测试平台提供了一种方法,通过一个简单的接口来使用 async/await 模式初始化扩展。 为了保持对称性,它还为清理提供了异步接口,插件可以无缝地实现这个接口。

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 TestHostTestHostController 中的 ITestApplicationBuilder 对象注册扩展本身,详见相应部分。

但是,如果需要在两个扩展之间“共享状态”,则可以实现和注册实现不同接口的不同对象,这会使得共享成为一项具有挑战性的任务。 如果没有任何协助,就需要一种方法将一个扩展传递给另一个扩展以共享信息,而这就会让设计变得复杂。

为此,测试平台提供了使用同一类型实现多个扩展点的复杂方法,使数据共享成为一项简单的任务。 只需利用 CompositeExtensionFactory<T>,然后就可以使用与单一接口实现相同的 API 进行注册。

例如,考虑一种同时实现 ITestSessionLifetimeHandlerIDataConsumer 的类型。 这种情况很常见,因为通常希望从测试框架中收集信息,然后在测试会话结束后,使用 IMessageBus 中的 ITestSessionLifetimeHandler.OnTestSessionFinishingAsync 来调度工件。

你应该做的是按常规实现这些接口:

internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ...
{
   ...
}

为类型创建 CompositeExtensionFactory<CustomExtension> 后,就可以用 IDataConsumerITestSessionLifetimeHandler API 来注册它,这两个 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,可以将 IDataConsumerITestSessionLifetimeHandler 结合起来。
  • 对于 ITestApplicationBuilder.TestHostControllers,可以将 ITestHostEnvironmentVariableProviderITestHostProcessLifetimeHandler 结合起来。