Compartilhar via


Criar extensões para Microsoft.Testing.Platform

Este artigo aborda os pontos de extensibilidade para Microsoft.Testing.Platform além da própria estrutura de teste. Para a criação da estrutura de teste, consulte Criar uma estrutura de teste.

Para obter um resumo completo dos pontos de extensão e dos conceitos de processo interno/externo, consulte Criar extensões personalizadas.

Pontos de extensibilidade

A plataforma de testes oferece pontos de extensibilidade adicionais que permitem personalizar o comportamento da plataforma e da estrutura de testes. Esses pontos de extensibilidade são opcionais e podem ser usados para aprimorar a experiência de teste.

As extensões ICommandLineOptionsProvider

Observação

Ao estender essa API, a extensão personalizada existirá dentro e fora do processo do host de teste.

Conforme discutido na seção de arquitetura, a etapa inicial envolve a criação do ITestApplicationBuilder para registrar a estrutura de teste e as extensões com ela.

var builder = await TestApplication.CreateBuilderAsync(args);

O método CreateBuilderAsync aceita uma matriz de cadeias de caracteres (string[]) chamada args. Esses argumentos podem ser usados para passar opções de linha de comando para todos os componentes da plataforma de teste (incluindo componentes integrados, estruturas de teste e extensões), permitindo a personalização de seu comportamento.

Normalmente, os argumentos passados são aqueles recebidos no método Main(string[] args) padrão. No entanto, se o ambiente de hospedagem for diferente, qualquer lista de argumentos poderá ser fornecida.

Os argumentos devem ser prefixados com um traço duplo --. Por exemplo, --filter.

Se um componente, como uma estrutura de teste ou um ponto de extensão, desejar oferecer opções de linha de comando personalizadas, ele poderá fazer isso implementando a interface ICommandLineOptionsProvider . Essa implementação pode ser registrada com o ITestApplicationBuilder pela fábrica de registro da propriedade CommandLine, conforme mostrado:

builder.CommandLine.AddProvider(
    static () => new CustomCommandLineOptions());

No exemplo fornecido, CustomCommandLineOptions é uma implementação da interface ICommandLineOptionsProvider. Essa interface compreende os seguintes membros e tipos de dados:

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

Conforme observado, o ICommandLineOptionsProvider estende a interface IExtension. Portanto, como qualquer outra extensão, você pode optar por ativá-la ou desativá-la usando a API IExtension.IsEnabledAsync.

A ordem de execução de ICommandLineOptionsProvider é:

Um diagrama que representa a ordem de execução da interface “ICommandLineOptionsProvider”.

Vamos examinar as APIs e seu significado:

ICommandLineOptionsProvider.GetCommandLineOptions(): este método é usado para recuperar todas as opções oferecidas pelo componente. Cada CommandLineOption requer que as seguintes propriedades sejam especificadas:

string name: este é o nome da opção, apresentado sem um traço. Por exemplo, o filtro seria usado como --filter pelos usuários.

string description: esta é uma descrição da opção. Ele será exibido quando os usuários passarem --help como um argumento para o construtor de aplicativos.

ArgumentArity arity: a aridade de uma opção é o número de valores que podem ser passados se essa opção ou comando for especificado. As aridades disponíveis atualmente são:

  • Zero: representa uma aridade de argumento igual a zero.
  • ZeroOrOne: representa uma aridade de argumento entre zero e um.
  • ZeroOrMore: representa uma aridade de argumento de zero ou mais.
  • OneOrMore: representa a aridade de argumento de um ou mais.
  • ExactlyOne: representa uma aridade de argumento de exatamente um.

Para obter exemplos, consulte a tabela de aridade System.CommandLine.

bool isHidden: essa propriedade significa que a opção está disponível para uso, mas não será exibida na descrição quando --help for invocada.

ICommandLineOptionsProvider.ValidateOptionArgumentsAsync: este método é empregado para validar o argumento fornecido pelo usuário.

Por exemplo, se você tiver um parâmetro chamado --dop que represente o grau de paralelismo para nossa estrutura de teste personalizada, um usuário poderá inserir --dop 0. Nesse cenário, o valor 0 seria inválido porque espera-se que tenha um grau de paralelismo igual 1 ou superior. Usando ValidateOptionArgumentsAsync, você pode executar a validação inicial e retornar uma mensagem de erro, se necessário.

Uma possível implementação para o exemplo acima poderia ser:

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: este método é chamado de último e permite fazer a verificação de coerência global.

Por exemplo, digamos que nossa estrutura de teste tenha a capacidade de gerar um relatório de resultados de teste e salvá-lo em um arquivo. Esse recurso é acessado usando a opção --generatereport e o nome do arquivo é especificado com --reportfilename myfile.rep. Nesse cenário, se um usuário fornecer apenas a opção --generatereport sem especificar um nome de arquivo, a validação deverá falhar porque o relatório não pode ser gerado sem um nome de arquivo. Uma possível implementação para o exemplo acima poderia ser:

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

Observe que o método ValidateCommandLineOptionsAsync fornece o serviço ICommandLineOptions, que é usado para buscar as informações do argumento analisadas pela própria plataforma.

As extensões ITestSessionLifetimeHandler

O ITestSessionLifeTimeHandler é uma extensão em processo que permite a execução de código antes e depois da sessão de teste.

Para registrar um ITestSessionLifeTimeHandler personalizado, use a seguinte API:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHost.AddTestSessionLifetimeHandle(
    static serviceProvider => new CustomTestSessionLifeTimeHandler());

A fábrica utiliza o IServiceProvider para obter acesso ao conjunto de serviços oferecidos pela plataforma de teste.

Importante

A sequência de registro é significativa, pois as APIs são chamadas na ordem em que foram registradas.

A interface ITestSessionLifeTimeHandler inclui os seguintes métodos:

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

O ITestSessionLifetimeHandler é um tipo de ITestHostExtension, que serve com base para todas as extensões de host de teste. Como todos os outros pontos de extensão, ele também herda de IExtension. Portanto, como qualquer outra extensão, você pode optar por ativá-la ou desativá-la usando a API IExtension.IsEnabledAsync.

Considere os seguintes detalhes para esta API:

OnTestSessionStartingAsync: este método é invocado antes do início da sessão de teste e recebe o objeto SessionUid, que fornece um identificador opaco para a sessão de teste atual.

OnTestSessionFinishingAsync: este método é invocado após a conclusão da sessão de teste, garantindo que a estrutura de teste tenha concluído a execução de todos os testes e relatado todos os dados relevantes para a plataforma. Normalmente, nesse método, a extensão emprega o IMessageBus para transmitir ativos ou dados personalizados para o barramento de plataforma compartilhado. Esse método também pode sinalizar para qualquer extensão personalizada fora do processo que a sessão de teste foi concluída.

Por fim, ambas as APIs recebem um CancellationToken que a extensão deve honrar.

Se sua extensão exigir inicialização intensiva e você precisar usar o padrão async/await, consulte o Async extension initialization and cleanup. Se precisar compartilhar o estado entre pontos de extensão, consulte a seção CompositeExtensionFactory<T>.

As extensões ITestApplicationLifecycleCallbacks

O ITestApplicationLifecycleCallbacks é uma extensão em processo que permite a execução de código antes de tudo, é como ter acesso à primeira linha do hipotético main do host de teste.

Para registrar um ITestApplicationLifecycleCallbacks personalizado, use a seguinte API:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHost.AddTestApplicationLifecycleCallbacks(
    static serviceProvider
    => new CustomTestApplicationLifecycleCallbacks());

A fábrica utiliza o IServiceProvider para obter acesso ao conjunto de serviços oferecidos pela plataforma de teste.

Importante

A sequência de registro é significativa, pois as APIs são chamadas na ordem em que foram registradas.

A interface ITestApplicationLifecycleCallbacks inclui os seguintes métodos:

public interface ITestApplicationLifecycleCallbacks : ITestHostExtension
{
    Task BeforeRunAsync(CancellationToken cancellationToken);

    Task AfterRunAsync(
        int exitCode,
        CancellationToken cancellation);
}

public interface ITestHostExtension : IExtension
{
}

O ITestApplicationLifecycleCallbacks é um tipo de ITestHostExtension, que serve com base para todas as extensões de host de teste. Como todos os outros pontos de extensão, ele também herda de IExtension. Portanto, como qualquer outra extensão, você pode optar por ativá-la ou desativá-la usando a API IExtension.IsEnabledAsync.

BeforeRunAsync: esse método serve como o ponto de contato inicial para o host de teste e é a primeira oportunidade para uma extensão em processo executar um recurso. Normalmente, é usado para estabelecer uma conexão com qualquer extensão fora do processo correspondente se um recurso for projetado para operar em ambos os ambientes.

Por exemplo, o recurso integrado de despejo de suspensão é composto de extensões em processo e fora do processo, e esse método é usado para trocar informações com o componente fora do processo da extensão.

AfterRunAsync: este método é a chamada final antes de sair do int ITestApplication.RunAsync() e fornece o exit code. Deve ser usado exclusivamente para tarefas de limpeza e para notificar qualquer extensão fora do processo correspondente de que o host de teste está prestes a ser encerrado.

Por fim, ambas as APIs recebem um CancellationToken que a extensão deve honrar.

As extensões IDataConsumer

O IDataConsumer é uma extensão em processo capaz de assinar e receber IData informações que são enviadas por push para o IMessageBus pela estrutura de teste e suas extensões.

Esse ponto de extensão é fundamental, pois permite que os desenvolvedores coletem e processem todas as informações geradas durante uma sessão de teste.

Para registrar um IDataConsumer personalizado, use a seguinte API:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHost.AddDataConsumer(
    static serviceProvider => new CustomDataConsumer());

A fábrica utiliza o IServiceProvider para obter acesso ao conjunto de serviços oferecidos pela plataforma de teste.

Importante

A sequência de registro é significativa, pois as APIs são chamadas na ordem em que foram registradas.

A interface IDataConsumer inclui os seguintes métodos:

public interface IDataConsumer : ITestHostExtension
{
    Type[] DataTypesConsumed { get; }

    Task ConsumeAsync(
        IDataProducer dataProducer,
        IData value,
        CancellationToken cancellationToken);
}

public interface IData
{
    string DisplayName { get; }
    string? Description { get; }
}

O IDataConsumer é um tipo de ITestHostExtension, que serve com base para todas as extensões de host de teste. Como todos os outros pontos de extensão, ele também herda de IExtension. Portanto, como qualquer outra extensão, você pode optar por ativá-la ou desativá-la usando a API IExtension.IsEnabledAsync.

DataTypesConsumed: essa propriedade retorna uma lista de Type que essa extensão planeja consumir. Isso corresponde ao IDataProducer.DataTypesProduced. Notavelmente, um IDataConsumer pode se inscrever em vários tipos originados de diferentes instâncias IDataProducer sem problemas.

ConsumeAsync: esse método é acionado sempre que os dados de um tipo para o qual o consumidor atual está inscrito são enviados para o IMessageBus. Ele recebe o IDataProducer para fornecer detalhes sobre o produtor da carga útil, bem como a própria carga útil IData. Como você pode ver, IData é uma interface genérica de placeholder que contém dados informativos gerais. A capacidade de enviar diferentes tipos de IData implica que o consumidor precisa ativar o próprio tipo para convertê-lo no tipo correto e acessar as informações específicas.

Um exemplo de implementação de um consumidor que deseja elaborar o TestNodeUpdateMessage produzido por uma estrutura de teste pode ser:

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

Por fim, a API usa um CancellationToken que a extensão deve respeitar.

Importante

É fundamental processar o payload diretamente dentro do método ConsumeAsync. O IMessageBus pode gerenciar o processamento síncrono e assíncrono, coordenando a execução com a estrutura de teste. Embora o processo de consumo seja totalmente assíncrono e não bloqueie o IMessageBus.Push no momento em que este artigo foi escrito, esse é um detalhe de implementação que pode mudar no futuro devido a requisitos futuros. No entanto, a plataforma garante que esse método seja sempre chamado uma vez, eliminando a necessidade de sincronização complexa, além de gerenciar a escalabilidade dos consumidores.

Aviso

Ao utilizar IDataConsumer em conjunto com ITestHostProcessLifetimeHandler em um ponto de extensão composto, é crucial desconsiderar todos os dados recebidos após a execução de ITestSessionLifetimeHandler.OnTestSessionFinishingAsync. A OnTestSessionFinishingAsync é a oportunidade final de processar dados acumulados e transmitir novas informações para o IMessageBus. Portanto, todos os dados consumidos além desse ponto não serão utilizáveis pela extensão.

Se sua extensão exigir inicialização intensiva e você precisar usar o padrão async/await, consulte o Async extension initialization and cleanup. Se precisar compartilhar o estado entre pontos de extensão, consulte a seção CompositeExtensionFactory<T>.

As extensões ITestHostEnvironmentVariableProvider

O ITestHostEnvironmentVariableProvider é uma extensão fora do processo que permite estabelecer variáveis de ambiente personalizadas para o host de teste. A utilização desse ponto de extensão garante que a plataforma de teste inicie um novo host com as variáveis de ambiente apropriadas, conforme detalhado na seção de arquitetura.

Para registrar um ITestHostEnvironmentVariableProvider personalizado, use a seguinte API:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHostControllers.AddEnvironmentVariableProvider(
    static serviceProvider => new CustomEnvironmentVariableForTestHost());

A fábrica utiliza o IServiceProvider para obter acesso ao conjunto de serviços oferecidos pela plataforma de teste.

Importante

A sequência de registro é significativa, pois as APIs são chamadas na ordem em que foram registradas.

A interface ITestHostEnvironmentVariableProvider inclui os seguintes métodos e tipos:

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

O ITestHostEnvironmentVariableProvider é um tipo de ITestHostControllersExtension, que serve com base para todas as extensões do controlador de host de teste. Como todos os outros pontos de extensão, ele também herda de IExtension. Portanto, como qualquer outra extensão, você pode optar por ativá-la ou desativá-la usando a API IExtension.IsEnabledAsync.

Considere os detalhes dessa API:

UpdateAsync: essa API de atualização fornece uma instância do objeto IEnvironmentVariables a partir do qual você pode chamar os métodos SetVariable ou RemoveVariable. Ao utilizar SetVariable, você deve passar um objeto do tipo EnvironmentVariable, que requer as seguintes especificações:

  • Variable: o nome da variável de ambiente.
  • Value: o valor da variável de ambiente.
  • IsSecret: indica se a variável de ambiente contém informações confidenciais que não devem ser registradas ou acessíveis por meio do TryGetVariable.
  • IsLocked:isso determina se outras extensões ITestHostEnvironmentVariableProvider podem modificar esse valor.

ValidateTestHostEnvironmentVariablesAsync: este método é invocado depois que todos os métodos UpdateAsync das instâncias ITestHostEnvironmentVariableProvider registradas foram chamados. Permite verificar a configuração correta das variáveis de ambiente. Utiliza um objeto que implementa IReadOnlyEnvironmentVariables, que fornece o método TryGetVariable para buscar informações específicas da variável de ambiente com o tipo de objeto OwnedEnvironmentVariable. Após a validação, você retorna um ValidationResult com todos os motivos de falha.

Observação

A plataforma de teste, por padrão, implementa e registra o SystemEnvironmentVariableProvider. Esse provedor carrega todas as variáveis de ambiente atuais. Como o primeiro provedor registrado, ele é executado primeiro, concedendo acesso às variáveis de ambiente padrão para todas as outras extensões de usuário ITestHostEnvironmentVariableProvider.

Se sua extensão exigir inicialização intensiva e você precisar usar o padrão async/await, consulte o Async extension initialization and cleanup. Se precisar compartilhar o estado entre pontos de extensão, consulte a seção CompositeExtensionFactory<T>.

As extensões ITestHostProcessLifetimeHandler

O ITestHostProcessLifetimeHandler é uma extensão fora do processo que permite observar o processo do host de teste de um ponto de vista externo. Isso garante que sua extensão não seja afetada por possíveis falhas ou travamentos que poderiam ser induzidos pelo código em teste. A utilização desse ponto de extensão solicitará que a plataforma de teste inicie um novo host, conforme detalhado na seção de arquitetura .

Para registrar um ITestHostProcessLifetimeHandler personalizado, use a seguinte API:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

builder.TestHostControllers.AddProcessLifetimeHandler(
    static serviceProvider => new CustomMonitorTestHost());

A fábrica utiliza o IServiceProvider para obter acesso ao conjunto de serviços oferecidos pela plataforma de teste.

Importante

A sequência de registro é significativa, pois as APIs são chamadas na ordem em que foram registradas.

A interface ITestHostProcessLifetimeHandler inclui os seguintes métodos:

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

O ITestHostProcessLifetimeHandler é um tipo de ITestHostControllersExtension, que serve com base para todas as extensões do controlador de host de teste. Como todos os outros pontos de extensão, ele também herda de IExtension. Portanto, como qualquer outra extensão, você pode optar por ativá-la ou desativá-la usando a API IExtension.IsEnabledAsync.

Considere os seguintes detalhes para esta API:

BeforeTestHostProcessStartAsync: esse método é invocado antes da plataforma de teste iniciar os hosts de teste.

OnTestHostProcessStartedAsync: esse método é invocado imediatamente após o início do host de teste. Esse método oferece um objeto que implementa a interface ITestHostProcessInformation, que fornece detalhes importantes sobre o resultado do processo do host de teste.

Importante

A invocação desse método não interrompe a execução do host de teste. Se você precisar pausá-lo, registre uma extensão em processo, como ITestApplicationLifecycleCallbacks e sincronize-a com a extensão fora do processo.

OnTestHostProcessExitedAsync: este método é chamado quando a execução da suíte de testes é concluída. Esse método fornece um objeto que adere à interface ITestHostProcessInformation, que transmite detalhes fundamentais sobre o resultado do processo do host de teste.

A interface ITestHostProcessInformation fornece os seguintes detalhes:

  • PID: O ID do processo do host de teste.
  • ExitCode: o código de saída do processo. Esse valor só está disponível dentro do método OnTestHostProcessExitedAsync. A tentativa de acessá-lo dentro do método OnTestHostProcessStartedAsync resultará em uma exceção.
  • HasExitedGracefully: um valor booleano que indica se o host de teste sofreu uma falha. Se for verdadeiro, significa que o host de teste não foi encerrado de forma normal.

Ordem de execução de extensões

A plataforma de teste consiste em uma estrutura de teste e qualquer número de extensões que possam operar dentro ou fora do processo. Este documento descreve a sequência de chamadas para todos os pontos de extensibilidade potenciais para fornecer clareza sobre quando um recurso deve ser invocado:

  1. ITestHostEnvironmentVariableProvider.UpdateAsync : fora do processo
  2. ITestHostEnvironmentVariableProvider.ValidateTestHostEnvironmentVariablesAsync : fora do processo
  3. ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync : fora do processo
  4. Início do processo de hospedagem de teste
  5. ITestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync: fora do processo, esse evento pode entrelaçar as ações de extensões em processo, dependendo das condições de corrida.
  6. ITestApplicationLifecycleCallbacks.BeforeRunAsync: em processo
  7. ITestSessionLifetimeHandler.OnTestSessionStartingAsync: em processo
  8. ITestFramework.CreateTestSessionAsync: em processo
  9. ITestFramework.ExecuteRequestAsync: em processo, esse método pode ser chamado uma ou mais vezes. Neste ponto, a estrutura de teste transmitirá informações para o IMessageBus que podem ser utilizadas pelo IDataConsumer.
  10. ITestFramework.CloseTestSessionAsync: em processo
  11. ITestSessionLifetimeHandler.OnTestSessionFinishingAsync: em execução
  12. ITestApplicationLifecycleCallbacks.AfterRunAsync: em execução
  13. A limpeza no processo envolve chamar 'dispose' e IAsyncCleanableExtension em todos os pontos de extensão.
  14. ITestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync : fora do processo
  15. A limpeza fora do processo envolve descartar e IAsyncCleanableExtension em todos os pontos de extensão.

Auxiliares de extensões

A plataforma de testes fornece um conjunto de classes auxiliares e interfaces para simplificar a implementação de extensões. Esses auxiliares são projetados para simplificar o processo de desenvolvimento e garantir que a extensão esteja de acordo com os padrões da plataforma.

Inicialização assíncrona e limpeza de extensões

A criação da estrutura de teste e extensões por meio de fábricas adere ao mecanismo padrão de criação de objetos .NET, que usa construtores síncronos. Se uma extensão exigir inicialização intensiva (como acessar o sistema de arquivos ou a rede), ela não poderá empregar o padrão async/await no construtor porque os construtores retornam void, não Task.

Portanto, a plataforma de testes fornece um método para inicializar uma extensão usando o padrão async/await por meio de uma interface simples. Por simetria, ele também oferece uma interface assíncrona para limpeza que as extensões podem implementar sem problemas.

public interface IAsyncInitializableExtension
{
    Task InitializeAsync();
}

public interface IAsyncCleanableExtension
{
    Task CleanupAsync();
}

IAsyncInitializableExtension.InitializeAsync: é garantido que este método será invocado após a factória de criação.

IAsyncCleanableExtension.CleanupAsync: esse método deve ser invocado pelo menos uma vez durante o término da sessão de teste, antes do padrão DisposeAsync ou Dispose.

Importante

Similar ao método Dispose padrão, CleanupAsync pode ser invocado várias vezes. Se o método CleanupAsync de um objeto for chamado mais de uma vez, o objeto deverá ignorar todas as chamadas após a primeira. O objeto não deve lançar uma exceção se seu método CleanupAsync for chamado várias vezes.

Observação

Por padrão, a plataforma de teste chamará DisposeAsync se estiver disponível ou Dispose se estiver implementada. É importante observar que a plataforma de testes não chamará os dois métodos de descarte, mas priorizará o método assíncrono, se implementado.

O CompositeExtensionFactory<T>

Conforme descrito na seção de extensões, a plataforma de teste permite que você implemente interfaces para incorporar extensões personalizadas dentro e fora do processo.

Cada interface aborda um recurso específico e, de acordo com .NET design, você implementa essa interface em um objeto específico. Você pode registrar a extensão usando a API de registro específica AddXXX do TestHost ou do objeto TestHostController do ITestApplicationBuilder, conforme detalhado nas seções correspondentes.

No entanto, se você precisar compartilhar o estado entre duas extensões, o fato de poder implementar e registrar objetos diferentes implementando interfaces diferentes torna o compartilhamento uma tarefa desafiadora. Sem qualquer assistência, você precisaria de uma maneira de passar uma extensão para a outra para compartilhar informações, o que complica o design.

Portanto, a plataforma de testes oferece um método sofisticado para implementar vários pontos de extensão usando o mesmo tipo, tornando o compartilhamento de dados uma tarefa simples. Tudo o que você precisa fazer é utilizar o CompositeExtensionFactory<T>, que pode ser registrado usando a mesma API que você faria para uma única implementação de interface.

Por exemplo, considere um tipo que implementa ITestSessionLifetimeHandler e IDataConsumer. Esse é um cenário comum, pois muitas vezes você deseja coletar informações da estrutura de teste e, em seguida, quando a sessão de teste for concluída, você enviará seu artefato usando o IMessageBus dentro do ITestSessionLifetimeHandler.OnTestSessionFinishingAsync.

O que você deve fazer é implementar normalmente as interfaces:

internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ...
{
   ...
}

Depois de criar o CompositeExtensionFactory<CustomExtension> para o seu tipo, você pode registrá-lo nas APIs IDataConsumer e ITestSessionLifetimeHandler, que oferecem uma sobrecarga para o CompositeExtensionFactory<T>:

var builder = await TestApplication.CreateBuilderAsync(args);

// ...

var factory = new CompositeExtensionFactory<CustomExtension>(serviceProvider => new CustomExtension());

builder.TestHost.AddTestSessionLifetimeHandle(factory);
builder.TestHost.AddDataConsumer(factory);

O construtor de fábrica usa o IServiceProvider para acessar os serviços fornecidos pela plataforma de teste.

A plataforma de testes será responsável por gerenciar o ciclo de vida da extensão composta.

É importante observar que, devido ao suporte da plataforma de teste para extensões em processo e fora do processo, você não pode combinar nenhum ponto de extensão arbitrariamente. A criação e a utilização de extensões dependem do tipo de host, o que significa que você só pode agrupar extensões dentro (TestHost) e fora do processo (TestHostController).

As seguintes combinações são possíveis:

  • Para ITestApplicationBuilder.TestHost, você pode combinar IDataConsumer e ITestSessionLifetimeHandler.
  • Para ITestApplicationBuilder.TestHostControllers, você pode combinar ITestHostEnvironmentVariableProvider e ITestHostProcessLifetimeHandler.