Partilhar via


Gerir o host da aplicação nos testes .NET.NET Aspire

Ao escrever testes funcionais ou de integração com .NET.NET Aspire, gerir a instância do host da aplicação de forma eficiente é crucial. O host do aplicativo representa todo o ambiente do aplicativo e pode ser caro para criar e desmontar. Este artigo explica como gerenciar a instância de host do aplicativo em seus testes de .NET.NET Aspire.

Para escrever testes com .NET.NET Aspire, use o pacote NuGet 📦 Aspire.Hosting.Testing que contém algumas classes auxiliares para gerenciar a instância de host do aplicativo em seus testes.

Utilize a classe DistributedApplicationTestingBuilder

No tutorial sobre como escrever o seu primeiro teste, você foi apresentado à classe DistributedApplicationTestingBuilder que pode ser usada para criar a instância do anfitrião da aplicação:

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

O método DistributedApplicationTestingBuilder.CreateAsync<T> usa o tipo de projeto de host de aplicativo como um parâmetro genérico para criar a instância de host de aplicativo. Embora esse método seja executado no início de cada teste, é mais eficiente criar a instância do host do aplicativo uma vez e compartilhá-la entre testes à medida que o conjunto de testes cresce.

Com xUnit, você implementa a interface IAsyncLifetime na classe de testes para suportar a inicialização assíncrona e o descarte da instância do host do aplicativo. O método InitializeAsync é usado para criar a instância do host do aplicativo antes que os testes sejam executados e o método DisposeAsync descarta o host do aplicativo assim que os testes forem concluídos.

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

Com o MSTest, você usa o ClassInitializeAttribute e ClassCleanupAttribute em métodos estáticos da classe de teste para fornecer a inicialização e a limpeza da instância do host do aplicativo. O método ClassInitialize é usado para criar a instância do host do aplicativo antes que os testes sejam executados e o método ClassCleanup descarta a instância do host do aplicativo assim que os testes forem concluídos.

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

Com o NUnit, utiliza os atributos OneTimeSetUp e OneTimeTearDown em métodos da classe de teste para configurar e desmontar a instância do host da aplicação. O método OneTimeSetUp é usado para criar a instância do host do aplicativo antes que os testes sejam executados e o método OneTimeTearDown descarta a instância do host do aplicativo assim que os testes forem concluídos.

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

Ao capturar o host do aplicativo em um campo quando a execução do teste é iniciada, você pode acessá-lo em cada teste sem a necessidade de recriá-lo, diminuindo o tempo necessário para executar os testes. Em seguida, quando a execução de teste é concluída, o host do aplicativo é descartado, o que limpa todos os recursos que foram criados durante a execução de teste, como contêineres.

Passar argumentos para o host do aplicativo

Você pode acessar os argumentos do host do aplicativo com o parâmetro args. Os argumentos também são passados para o sistema de configuração do .NET, para que possas alterar muitas definições de configuração dessa maneira. No exemplo a seguir, você substitui o ambiente especificando-o como uma opção de linha de comando:

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

Outros argumentos podem ser passados para o host do aplicativo Program e disponibilizados no host do aplicativo. No exemplo seguinte, você passa um argumento para o host do aplicativo e o usa para controlar se adiciona volumes de dados a uma instância Postgres.

No host do aplicativo Program, você usa a configuração para dar suporte à habilitação ou desativação de volumes:

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

No código de teste, você passa "UseVolumes=false" no args para o host do aplicativo:

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

Utilize a classe DistributedApplicationFactory

Embora a classe DistributedApplicationTestingBuilder seja útil para muitos cenários, pode haver situações em que você queira ter mais controle sobre a inicialização do host do aplicativo, como a execução de código antes que o construtor seja criado ou depois que o host do aplicativo for criado. Nesses casos, você implementa sua própria versão da classe DistributedApplicationFactory. É isso que o DistributedApplicationTestingBuilder usa internamente.

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

O construtor requer o tipo de referência do projeto de host do aplicativo como um parâmetro. Opcionalmente, você pode fornecer argumentos para o construtor de aplicativos de host subjacente. Esses argumentos controlam como o host do aplicativo é iniciado e fornecem valores para a variável args usada pelo arquivo Program.cs para iniciar a instância do host do aplicativo.

Métodos de ciclo de vida

A classe DistributionApplicationFactory fornece vários métodos de ciclo de vida que podem ser substituídos para fornecer comportamento personalizado durante toda a preparação e criação do host do aplicativo. Os métodos disponíveis são OnBuilderCreating, OnBuilderCreated, OnBuildinge OnBuilt.

Por exemplo, podemos usar o método OnBuilderCreating para definir a configuração, como as informações de assinatura e grupo de recursos para Azure, antes que o host do aplicativo seja criado e quaisquer recursos de Azure dependentes sejam provisionados, resultando em nossos testes usando o ambiente de Azure correto.

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

Devido à ordem de precedência no sistema de configuração .NET, as variáveis de ambiente serão usadas sobre qualquer coisa no arquivo appsettings.json ou secrets.json.

Outro cenário que você pode querer usar no ciclo de vida é configurar os serviços usados pelo host do aplicativo. No exemplo a seguir, considere um cenário em que você substitui a API OnBuilderCreated para adicionar resiliência ao HttpClient:

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

Ver também