在 .NET.NET Aspire 测试中管理应用主机

使用 .NET.NET Aspire编写功能测试或集成测试时,有效管理 应用主机 实例至关重要。 应用主机表示完整的应用程序环境,创建和拆解成本可能很高。 本文介绍如何在 .NET.NET Aspire 测试中管理应用主机实例。

若要使用 .NET.NET Aspire编写测试,请使用 📦 Aspire.Hosting.Testing NuGet 包,其中包含一些帮助程序类来管理测试中的应用主机实例。

使用 DistributedApplicationTestingBuilder

在编写第一个测试的 教程中,你已介绍可用于创建应用主机实例的 类:

var appHost = await DistributedApplicationTestingBuilder
    .CreateAsync<Projects.AspireApp_AppHost>();

DistributedApplicationTestingBuilder.CreateAsync<T> 方法采用应用主机项目类型作为通用参数来创建应用主机实例。 虽然此方法在每个测试开始时执行,但创建应用主机实例一次,并在测试套件增长时在测试之间共享它更为高效。

使用 xUnit,可以在测试类上实现 IAsyncLifetime 接口,以支持应用主机实例的异步初始化和处置。 InitializeAsync 方法用于在运行测试之前创建应用主机实例,DisposeAsync 方法在测试完成后释放应用主机。

public class WebTests : IAsyncLifetime
{
    private DistributedApplication _app;

    public async Task InitializeAsync()
    {
        var appHost = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.AspireApp_AppHost>();

        _app = await appHost.BuildAsync();
    }

    public async Task DisposeAsync() => await _app.DisposeAsync();

    [Fact]
    public async Task GetWebResourceRootReturnsOkStatusCode()
    {
        // test code here
    }
}

使用 MSTest,可以在测试类的静态方法上使用 ClassInitializeAttributeClassCleanupAttribute 来提供应用主机实例的初始化和清理。 ClassInitialize 方法用于在运行测试之前创建应用主机实例,ClassCleanup 方法在测试完成后释放应用主机实例。

[TestClass]
public class WebTests
{
    private static DistributedApplication _app;

    [ClassInitialize]
    public static async Task ClassInitialize(TestContext context)
    {
       var appHost = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.AspireApp_AppHost>();

        _app = await appHost.BuildAsync();
    }

    [ClassCleanup]
    public static async Task ClassCleanup() => await _app.DisposeAsync();

    [TestMethod]
    public async Task GetWebResourceRootReturnsOkStatusCode()
    {
        // test code here
    }
}

使用 NUnit,可以在测试类的方法上使用 OneTimeSetUpOneTimeTearDown 属性来进行应用主机实例的初始化和清理。 OneTimeSetUp 方法用于在运行测试之前创建应用主机实例,OneTimeTearDown 方法在测试完成后释放应用主机实例。

public class WebTests
{
    private DistributedApplication _app;

    [OneTimeSetUp]
    public async Task OneTimeSetup()
    {
       var appHost = await DistributedApplicationTestingBuilder
            .CreateAsync<Projects.AspireApp_AppHost>();

        _app = await appHost.BuildAsync();
    }

    [OneTimeTearDown]
    public async Task OneTimeTearDown() => await _app.DisposeAsync();

    [Test]
    public async Task GetWebResourceRootReturnsOkStatusCode()
    {
        // test code here
    }
}

通过在启动测试运行时捕获字段中的应用主机,可以在每个测试中访问它,而无需重新创建它,从而减少运行测试所需的时间。 然后,测试运行完成后,将释放应用主机,这将清理在测试运行期间创建的任何资源,例如容器。

将参数传递给应用主机

可以使用 args 参数从应用主机访问参数。 参数也传递给 .NET的配置系统,因此你可以这样替代许多配置设置。 在以下示例中,通过将其指定为命令行选项来替代 环境

var builder = await DistributedApplicationTestingBuilder
    .CreateAsync<Projects.MyAppHost>(
    [
        "--environment=Testing"
    ]);

其他参数可以传递给应用主机 Program 并在应用主机中提供。 在下一个示例中,将参数传递给应用主机,并使用它来控制是否向 Postgres 实例添加数据卷。

在应用程序主机 Program中,使用配置来支持启用或禁用卷功能:

var postgres = builder.AddPostgres("postgres1");
if (builder.Configuration.GetValue("UseVolumes", true))
{
    postgres.WithDataVolume();
}

在测试代码中,你将 args 中的 "UseVolumes=false" 传递给应用主机:

public async Task DisableVolumesFromTest()
{
    // Disable volumes in the test builder via arguments:
    using var builder = await DistributedApplicationTestingBuilder
        .CreateAsync<Projects.TestingAppHost1_AppHost>(
        [
            "UseVolumes=false"
        ]);

    // The container will have no volume annotation since we disabled volumes by passing UseVolumes=false
    var postgres = builder.Resources.Single(r => r.Name == "postgres1");

    Assert.Empty(postgres.Annotations.OfType<ContainerMountAnnotation>());
}

使用 DistributedApplicationFactory

尽管 DistributedApplicationTestingBuilder 类适用于许多方案,但在某些情况下,你可能希望更好地控制启动应用主机,例如在创建生成器之前执行代码,或者在生成应用主机之后执行代码。 在这些情况下,你将实现自己的 DistributedApplicationFactory 类版本。 这是 DistributedApplicationTestingBuilder 在内部使用的内容。

public class TestingAspireAppHost()
    : DistributedApplicationFactory(typeof(Projects.AspireApp_AppHost))
{
    // override methods here
}

构造函数需要应用主机项目引用的类型作为参数。 (可选)可以向基础主机应用程序生成器提供参数。 这些参数控制应用主机的启动方式,并向 Program.cs 文件用来启动应用主机实例的 args 变量提供值。

生命周期方法

DistributionApplicationFactory 类提供了多个生命周期方法,这些方法可以被重写,从而在整个准备和创建应用主机的过程中实现自定义行为。 可用的方法是 OnBuilderCreatingOnBuilderCreatedOnBuildingOnBuilt

例如,我们可以使用 OnBuilderCreating 方法设置配置(例如 Azure的订阅和资源组信息),然后创建应用主机并预配任何依赖 Azure 资源,从而使用正确的 Azure 环境进行测试。

public class TestingAspireAppHost() : DistributedApplicationFactory(typeof(Projects.AspireApp_AppHost))
{
    protected override void OnBuilderCreating(DistributedApplicationOptions applicationOptions, HostApplicationBuilderSettings hostOptions)
    {
        hostOptions.Configuration ??= new();
        hostOptions.Configuration["environment"] = "Development";
        hostOptions.Configuration["AZURE_SUBSCRIPTION_ID"] = "00000000-0000-0000-0000-000000000000";
        hostOptions.Configuration["AZURE_RESOURCE_GROUP"] = "my-resource-group";
    }
}

由于 .NET 配置系统中的优先级顺序,环境变量的优先级高于 appsettings.jsonsecrets.json 文件中的任何内容。

你可能要在生命周期中使用的另一种方案是配置应用主机使用的服务。 在以下示例中,请考虑通过覆写 OnBuilderCreated API 来提高 HttpClient 的容错性:

protected override void OnBuilderCreated(
    DistributedApplicationBuilder applicationBuilder)
{
    applicationBuilder.Services.ConfigureHttpClientDefaults(clientBuilder =>
    {
        clientBuilder.AddStandardResilienceHandler();
    });
}

另请参阅