Microsoft.Testing.Platform 扩展性

Microsoft.Testing.Platform 由 测试框架 和任意数量的 扩展 组成,这些扩展可以运行 进程内进程外

体系结构 部分所述,Microsoft.Testing.Platform 旨在容纳各种方案和扩展点。 最重要的扩展无疑是测试将使用的测试框架。 注册失败会导致启动错误。 测试框架是执行测试会话所需的唯一强制性扩展。

为了支持诸如生成测试报告、代码覆盖率、重试失败测试和其他潜在功能等方案,需要提供一种机制,允许其他扩展与测试框架协同工作,以便提供测试框架本身并不提供的这些功能。

从本质上讲,测试框架是提供有关组成测试套件的每个测试的信息的主要扩展。 它会报告特定测试是否成功、失败或跳过,还能提供每个测试的其他信息,如一个用户可读的名称(称为显示名称)、源文件和测试开始的行等。

通过可扩展性点,可以利用测试框架提供的信息来生成新的工件,或利用附加功能来增强现有工件。 常用的扩展是 TRX 报告生成器,它会订阅 TestNodeUpdateMessage 并从中生成 XML 报告文件。

正如体系结构中所讨论的,某些扩展点“无法”测试框架在同一流程中运行。 原因通常包括:

  • 需要修改“测试主机”的“环境变量”。 在测试主机进程内采取行动“太晚”
  • 要求从外部“监控”进程,因为“测试主机”(测试和用户代码运行的地方)可能存在一些“用户代码错误”,使得进程本身“不稳定”,从而导致潜在的“挂起”或“崩溃”。 在这种情况下,扩展会与测试主机进程一起崩溃或挂起。

由于这些原因,扩展点被分为两类:

  1. 进程内扩展:这些扩展与测试框架在同一进程内运行。

    可以通过 属性注册“进程内扩展”ITestApplicationBuilder.TestHost

    // ...
    var builder = await TestApplication.CreateBuilderAsync(args);
    builder.TestHost.AddXXX(...);
    // ...
    
  2. 进程外扩展:这些扩展在单独的进程中运行,允许它们监控测试主机,而不受测试主机本身的影响。

    可以通过 来注册“进程外扩展”ITestApplicationBuilder.TestHostControllers

    var builder = await TestApplication.CreateBuilderAsync(args);
    builder.TestHostControllers.AddXXX(...);
    

    最后,有些扩展可以在两种情况下正常使用。 这些常用扩展在“主机”中的表现完全相同。 可以通过 TestHost 和 TestHostController 接口或直接在 ITestApplicationBuilder 级别注册这些扩展。 ICommandLineOptionsProvider 就是此类扩展的一个示例。

IExtension 接口

IExtension 接口是测试平台内所有可扩展性点的基础接口。 它主要用于获取有关扩展的描述性信息,而最重要的是,用于启用或禁用扩展本身。

考虑以下 IExtension 接口:

public interface IExtension
{
    string Uid { get; }
    string Version { get; }
    string DisplayName { get; }
    string Description { get; }
    Task<bool> IsEnabledAsync();
}
  • Uid:表示扩展的唯一标识符。 为此字符串选择一个唯一的值至关重要,可以避免与其他扩展发生冲突。

  • Version:表示接口的版本。 需要使用语义版本控制

  • DisplayName:一个用户友好的名称表示形式,将显示在日志中,并在使用 --info 命令行选项来请求信息时显示。

  • Description:扩展的说明,会在使用 --info 命令行选项请求信息时显示。

  • IsEnabledAsync():当扩展实例化时,测试平台会调用该方法。 如果该方法返回 false,则扩展将被排除。 这种方法通常根据配置文件或一些自定义命令行选项来做决定。 用户通常会在命令行中指定 --customExtensionOption,以选择进入扩展本身。

测试框架扩展

测试框架是为测试平台提供发现和执行测试能力的主要扩展。 测试框架负责将测试结果传回测试平台。 测试框架是执行测试会话所需的唯一强制性扩展。

注册测试框架

本部分介绍如何在测试平台上注册测试框架。 使用 TestApplication.RegisterTestFramework API 为每个测试应用程序生成器注册一个测试框架,如 Microsoft.Testing.Platform 体系结构 文档中所示。

注册 API 的定义如下:

ITestApplicationBuilder RegisterTestFramework(
    Func<IServiceProvider, ITestFrameworkCapabilities> capabilitiesFactory,
    Func<ITestFrameworkCapabilities, IServiceProvider, ITestFramework> adapterFactory);

RegisterTestFramework API 需要两个工厂:

  1. Func<IServiceProvider, ITestFrameworkCapabilities>:这是一个委托,它接受一个实现 IServiceProvider 接口的对象,并返回一个实现 ITestFrameworkCapabilities 接口的对象。 IServiceProvider 允许访问平台服务,如配置、日志记录器和命令行参数。

    ITestFrameworkCapabilities 接口用于向平台和扩展公布测试框架支持的功能。 它通过实施和支持特定行为,使平台和扩展能够正确交互。 要想更好地理解功能概念,请参阅相关部分。

  2. Func<ITestFrameworkCapabilities, IServiceProvider, ITestFramework>:这是一个委托,它接收一个 ITestFrameworkCapabilities 对象(即 Func<IServiceProvider, ITestFrameworkCapabilities> 返回的实例)和一个 IServiceProvider 以再次提供对平台服务的访问。 预期返回对象是实现 ITestFramework 接口的对象。 ITestFramework 会充当执行引擎,负责发现和运行测试,然后将结果传回测试平台。

平台需要将 ITestFrameworkCapabilities 的创建与 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 不应阻止测试会话的执行。 所有测试框架都应能够发现和运行测试。 其影响应仅限于在测试框架缺乏某种功能时可能选择退出的扩展。

创建测试框架

Microsoft.Testing.Platform.Extensions.TestFramework.ITestFramework 通过提供测试框架的扩展来实现:

public interface ITestFramework : IExtension
{
    Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context);
    Task ExecuteRequestAsync(ExecuteRequestContext context);
    Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context);
}

ITestFramework 接口继承自 IExtension 接口,这是所有扩展点都会继承的接口。 IExtension 用于检索扩展的名称和说明。 通过 IExtensionTask<bool> IsEnabledAsync() 还提供了在设置中动态启用或禁用扩展的一种方法。 如果没有特殊需要,请确保从该方法返回 true

CreateTestSessionAsync 方法

CreateTestSessionAsync 方法在测试会话开始时调用,并被用于初始化测试框架。 该 API 接受一个 CloseTestSessionContext 对象,并返回一个 CloseTestSessionResult 对象。

public sealed class CreateTestSessionContext : TestSessionContext
{
    public SessionUid SessionUid { get; }
    public ClientInfo Client { get; }
    public CancellationToken CancellationToken { get; }
}

public readonly struct SessionUid
{
    public string Value { get; }
}

public sealed class ClientInfo
{
    public string Id { get; }
    public string Version { get; }
}

SessionUid 会用作当前测试会话的唯一标识符,提供了与会话结果的逻辑联系。 ClientInfo 提供了调用测试框架的实体的详细信息。 测试框架可利用这些信息来修改其行为。 例如,在编写本文档时,控制台执行将报告客户端名称,如“testingplatform-console”。 CancellationToken 用于停止 CreateTestSessionAsync 的执行。

返回对象是 CloseTestSessionResult

public sealed class CreateTestSessionResult
{
    public string? WarningMessage { get; set; }
    public string? ErrorMessage { get; set; }
    public bool IsSuccess { get; set; }
}

IsSuccess 属性用于指示会话创建是否成功。 当返回 false 时,测试停止执行。

CloseTestSessionAsync 方法

CloseTestSessionAsync 方法在功能上与 CreateTestSessionAsync 方法并列,唯一的区别是对象名称不同。 有关详细信息,请参阅 CreateTestSessionAsync 部分。

ExecuteRequestAsync 方法

ExecuteRequestAsync 方法接受一个 ExecuteRequestContext 类型的对象。 正如其名称所示,此对象包含测试框架应执行的操作的具体内容。 ExecuteRequestContext 的定义是:

public sealed class ExecuteRequestContext
{
    public IRequest Request { get; }
    public IMessageBus MessageBus { get; }
    public CancellationToken CancellationToken { get; }
    public void Complete();
}

IRequest:这是任何类型请求的基本接口。 应将测试框架视为“进程内的有状态服务器”,其中的生命周期是:

表示测试框架生命周期的序列图。

上图显示,测试平台在创建测试框架实例后会发出三个请求。 测试框架会处理这些请求,并利用请求本身包含的 IMessageBus 服务来提供每个特定请求的结果。 一旦某个请求得到处理,测试框架就会调用 Complete() 方法,向测试平台表明该请求已得到满足。 测试平台会监控所有已发送的请求。 一旦所有请求都得到满足,它就会调用 CloseTestSessionAsync 并处置实例(如果实现了 IDisposable/IAsyncDisposable)。 显然,请求及其完成可以重叠,从而实现请求的并发和异步执行。

注释

目前,测试平台不会发送重叠请求,而是等待请求 >> 完成后再发送下一个请求。 然而,这种行为今后可能会改变。 对并发请求的支持将通过功能系统来确定。

IRequest 实现指定了需要满足的精确请求。 测试框架会识别请求类型并进行相应处理。 如果请求类型无法识别,就会出现异常。

有关可用请求的详细信息,请参阅 IRequest 部分。

IMessageBus:此服务与请求相关联,允许测试框架“异步”向测试平台发布有关正在进行的请求的信息。 消息总线是平台的中心枢纽,为所有平台组件和扩展之间的异步通信提供便利。 有关可发布到测试平台的信息的完整列表,请参阅 IMessageBus 部分。

CancellationToken:此令牌用于中断对某个请求的处理。

Complete():如上一序列所示,Complete 方法通知平台已成功处理请求,所有相关信息已传输到 IMessageBus 中。

警告

在请求中忽略调用 Complete() 会导致测试程序无响应。

要根据自己或用户的要求定制测试框架,可以在配置文件中使用个性化部分,或使用自定义命令行选项

处理请求

下一部分将详细介绍测试框架可能接收和处理的各种请求。

在进入下一部分之前,彻底理解 IMessageBus 的概念至关重要,它是向测试平台传递测试执行信息的基本服务。

TestSessionContext

TestSessionContext 是所有请求的共享属性,提供有关正在进行的测试会话的信息:

public class TestSessionContext
{
    public SessionUid SessionUid { get; }
    public ClientInfo Client { get; }
}

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

public sealed class ClientInfo
{
    public string Id { get; }
    public string Version { get; }
}

TestSessionContext 包括 SessionUid,后者是正在进行的测试会话的唯一标识符,有助于记录和关联测试会话数据。 它还包括 ClientInfo 类型,该类型提供有关测试会话“发起程序”的详细信息。 测试框架可根据测试会话“发起程序”的身份来选择不同的路径或发布不同的信息。

发现测试执行请求

public class DiscoverTestExecutionRequest
{
    // Detailed in the custom section below
    public TestSessionContext Session { get; }

    // This is experimental and intended for future use, please disregard for now.
    public ITestExecutionFilter Filter { get; }
}

DiscoverTestExecutionRequest 指示测试框架”发现“测试,并将此信息传递给 IMessageBus

如上一部分所述,被发现测试的属性是 DiscoveredTestNodeStateProperty。 以下是一个供参考的通用代码段:

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
{
    // Detailed in the custom section below
    public TestSessionContext Session { get; }

    // This is experimental and intended for future use, please disregard for now.
    public ITestExecutionFilter Filter { get; }
}

RunTestExecutionRequest 指示测试框架“执行“测试,并将此信息传递给 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));

TestNodeUpdateMessage 数据

IMessageBus 部分中所述,在使用消息总线之前,必须指定要提供的数据类型。 测试平台定义了一个已知的 TestNodeUpdateMessage 类型,用来表示“测试更新信息”的概念。

本文档的这一部分将介绍如何利用这些有效负载数据。 让我们来看看图面:

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
{
}
  • TestNodeUpdateMessageTestNodeUpdateMessage 由两个属性组成:一个 TestNode 和一个 ParentTestNodeUidParentTestNodeUid 表示一个测试可能有一个父测试,引入了“测试树”的概念,其中 TestNode 可以相互排列。 此结构允许根据节点之间的“树”关系对未来功能进行增强。 如果测试框架不需要测试树结构,则可以选择不使用测试树结构,只需将其设置为 null,从而得到一个直接的 TestNode 平面列表。

  • TestNodeTestNode 由三个属性组成,其中一个属性是 Uid 类型的 TestNodeUid。 此 Uid 用作节点的 UNIQUE STABLE ID。 术语 UNIQUE STABLE ID 意味着同一 TestNode 应在不同运行和操作系统中保持 IDENTICALUid 一致。 TestNodeUid 是测试平台接受其原样的一个“任意的不透明字符串”

重要

在测试领域,ID 的稳定性和唯一性至关重要。 它们可以精确定位执行单个测试,并允许 ID 作为测试的持久标识符,从而为强大的扩展和功能提供便利。

第二个属性是 DisplayName,这是测试的用户友好名称。 例如,执行 --list-tests 命令行时就会显示此名称。

第三个属性是 Properties,属于 PropertyBag 类型。 如代码所示,这是一个专门的属性包,用于保存 TestNodeUpdateMessage 的一般属性。 这意味着可以向实现占位符接口 IProperty 的节点附加任何属性。

测试平台会识别添加到 TestNode.Properties 中的特定属性,以确定测试是否通过、失败或跳过。

可以在 TestNodeUpdateMessage.TestNode部分中找到具有相对说明的可用属性的当前列表。

PropertyBag 类型通常可在每个 IData 中访问,并被用于存储平台和扩展可以查询的其他属性。 通过这种机制,我们可以利用新的信息来增强平台,而无需引入重大更改。 如果组件能识别该属性,则可以对其进行查询;否则,会将其忽略。

最后,本部分明确指出,测试框架实现需要像下面的示例一样,实现产生 IDataProducerTestNodeUpdateMessage

internal sealed class TestingFramework
    : ITestFramework, IDataProducer
{
   // ...

   public Type[] DataTypesProduced =>
   [
       typeof(TestNodeUpdateMessage)
   ];

   // ...
}

如果测试适配器要求在执行过程中发布“文件”,则可以在此源文件中找到识别的属性:https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/Messages/FileArtifacts.cs。 如你所见,可以笼统地提供文件资产,也可以将其与特定的 TestNode 关联。 请记住,如果要推送 SessionFileArtifact,必须事先向平台声明,如下所示:

internal sealed class TestingFramework
    : ITestFramework, IDataProducer
{
   // ...

   public Type[] DataTypesProduced =>
   [
       typeof(TestNodeUpdateMessage),
       typeof(SessionFileArtifact)
   ];

   // ...
}

已知属性

请求部分所述,测试平台通过识别添加到TestNodeUpdateMessage中的特定属性来确定TestNode的状态(例如,成功、失败、跳过等)。 这样,运行时就能在控制台中准确显示失败测试的列表及其相应信息,并为测试进程设置适当的退出代码。

在这一段中,我们将阐明各种已知的 IProperty 选项及其各自的含义。

有关已知属性的综合列表,请参阅 TestNodeProperties.cs。 如果发现缺少属性说明,请提出问题。

这些属性可分为以下几类:

  1. 一般信息:可包含在任何类型请求中的属性。
  2. 发现信息:在 DiscoverTestExecutionRequest 发现请求期间提供的属性。
  3. 执行信息:在测试执行请求 RunTestExecutionRequest 期间提供的属性。

一些属性是“必需”的,而其他属性则是可选的。 强制属性是提供基本测试功能所必需的,如报告失败的测试和显示整个测试会话是否成功。

而可选属性通过提供更多信息来增强测试体验。 在 IDE 方案(如 VS、VSCode 等)、控制台运行或支持需要更详细信息才能正确运行的特定扩展时,它们尤为有用。 但是,这些可选属性不会影响测试的执行。

注释

当扩展需要特定信息才能正确运行时,它们的任务就是提醒和管理异常情况。 如果扩展缺少必要的信息,则它不应导致测试执行失败,而应直接选择退出。

一般信息
public record KeyValuePairStringProperty(
    string Key,
    string Value)
        : IProperty;

KeyValuePairStringProperty 代表一般的键/值对数据。

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

TestFileLocationProperty 用于确定测试在源文件中的位置。 当发起程序是 Visual Studio 或 Visual Studio Code 等 IDE 时,这一点尤为有用。

public sealed record TestMethodIdentifierProperty(
    string AssemblyFullName,
    string Namespace,
    string TypeName,
    string MethodName,
    string[] ParameterTypeFullNames,
    string ReturnTypeFullName)

TestMethodIdentifierProperty 是测试方法的唯一标识符,符合 ECMA-335 标准。

注释

创建该属性所需的数据可以通过 .NET 反射功能,使用 System.Reflection 命名空间中的类型来轻松获取。

public sealed record TestMetadataProperty(
    string Key,
    string Value)

TestMetadataProperty 用来表达 的“特征”TestNode

发现信息
public sealed record DiscoveredTestNodeStateProperty(
    string? Explanation = null)
{
    public static DiscoveredTestNodeStateProperty CachedInstance { get; }
}

DiscoveredTestNodeStateProperty 表示已发现该 TestNode。 在将 DiscoverTestExecutionRequest 发送到测试框架时会用到它。 请记下 CachedInstance 属性提供的便捷缓存值。 此属性为“必需”

执行信息
public sealed record InProgressTestNodeStateProperty(
    string? Explanation = null)
{
    public static InProgressTestNodeStateProperty CachedInstance { get; }
}

InProgressTestNodeStateProperty 会通知测试平台 TestNode 已安排执行,并且目前正在进行中。 请记下 CachedInstance 属性提供的便捷缓存值。

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

TimingProperty 用于传递有关 TestNode 执行的时间详细信息。 它还允许通过 StepTimingInfo 为各个执行步骤计时。 当测试概念分为初始化、执行和清理等多个阶段时,这尤为有用。

每个 都“必须”“有且仅有一个”以下属性,并将 TestNode 的结果传送给测试平台。

public sealed record PassedTestNodeStateProperty(
    string? Explanation = null)
        : TestNodeStateProperty(Explanation)
{
    public static PassedTestNodeStateProperty CachedInstance
        { get; } = new PassedTestNodeStateProperty();
}

PassedTestNodeStateProperty 会通知测试平台该 TestNode 已通过。 请记下 CachedInstance 属性提供的便捷缓存值。

public sealed record SkippedTestNodeStateProperty(
    string? Explanation = null)
        : TestNodeStateProperty(Explanation)
{
    public static SkippedTestNodeStateProperty CachedInstance
        { get; } =  new SkippedTestNodeStateProperty();
}

SkippedTestNodeStateProperty 通知测试平台跳过了此 TestNode。 请记下 CachedInstance 属性提供的便捷缓存值。

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

FailedTestNodeStateProperty 通知测试平台此 TestNode 在断言后失败。

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

ErrorTestNodeStateProperty 通知测试平台此 TestNode 已失败。 这种失败类型不同于用于断言故障的 FailedTestNodeStateProperty。 例如,可以使用 ErrorTestNodeStateProperty 来报告测试初始化错误等问题。

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

TimeoutTestNodeStateProperty 通知测试平台此 TestNode 因超时而失败。 可以使用 Timeout 属性来报告超时。

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

CancelledTestNodeStateProperty 通知测试平台此 TestNode 因取消而失败。

其他扩展性点

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

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:表示参数实参数量为零或 1。
  • 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,则表示测试主机没有正常退出。

扩展执行顺序

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

  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一次此方法。

重要

与标准的 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 结合起来。