在 Aspire 测试中管理 AppHost

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

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

使用 DistributedApplicationTestingBuilder

编写第一次测试的教程中,你被介绍到 DistributedApplicationTestingBuilder 可用于创建 AppHost 实例的类:

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

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

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

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测试类的静态方法来提供 AppHost 实例的初始化和清理。 该方法 ClassInitialize 用于在测试运行之前创建 AppHost 实例,并在 ClassCleanup 测试完成后释放 AppHost 实例。

[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 属性来提供 AppHost 实例的设置和拆解。 该方法 OneTimeSetUp 用于在测试运行之前创建 AppHost 实例,并在 OneTimeTearDown 测试完成后释放 AppHost 实例。

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

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

将参数传递给 AppHost

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

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

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

在 AppHost Program中,使用配置来支持启用或禁用卷:

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

在测试代码中"UseVolumes=false",将传入 args AppHost:

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

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

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

生命周期方法

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

例如,我们可以在创建 AppHost 之前使用 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 文件中的任何内容。

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

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

另请参阅