Compartilhar via


Gerenciar o host do aplicativo em testes de .NET.NET Aspire

Ao escrever testes funcionais ou de integração com .NET.NET Aspire, o gerenciamento da instância do host do aplicativo com eficiência é crucial. O host do aplicativo representa o ambiente completo do aplicativo e pode ser caro para criar e derrubar. Este artigo explica como gerenciar a instância do 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 do host do aplicativo em seus testes.

Usar a classe DistributedApplicationTestingBuilder

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

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

O método DistributedApplicationTestingBuilder.CreateAsync<T> usa o tipo de projeto de host do aplicativo como um parâmetro genérico para criar a instância do host do 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 aumenta.

Com o xUnit, você implementa a interface IAsyncLifetime na classe de teste para dar suporte à inicialização assíncrona e à eliminação 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 depois que os testes são 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 realizar 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 depois que os testes são 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, você usa os atributos OneTimeSetUp e OneTimeTearDown em métodos da classe de teste para fornecer a configuração e desmontagem da instância do host do aplicativo. 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 depois que os testes são 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
    }
}

Capturando o host do aplicativo em um campo quando a execução de 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 do 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. Argumentos também são passados para o sistema de configuração de.NET, para que você possa substituir muitas configurações 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 próximo exemplo, você passa um argumento para o host do aplicativo e o usa para controlar se você 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 desabilitaçã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>());
}

Usar a classe DistributedApplicationFactory

Embora a classe DistributedApplicationTestingBuilder seja útil para muitos cenários, pode haver situações em que você deseja ter mais controle sobre como iniciar o host do aplicativo, como executar o código antes de o construtor ser criado ou depois que o host do aplicativo for criado. Nesses casos, você implementa sua própria versão da classe DistributedApplicationFactory. Isso é o 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 subjacente de aplicativos host. Esses argumentos controlam como o host do aplicativo inicia 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 a preparação e a 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 todos os recursos de Azure dependentes sejam provisionados, resultando em nossos testes usando o ambiente 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 em vez de qualquer coisa no arquivo appsettings.json ou secrets.json.

Outro cenário que talvez você queira 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();
    });
}

Consulte também