Partilhar via


Testes de integração no ASP.NET Core

Por Jos van der Til, Martin Costelloe Javier Calvarro Nelson.

Os testes de integração garantem que os componentes de um aplicativo funcionem corretamente em um nível que inclua a infraestrutura de suporte do aplicativo, como o banco de dados, o sistema de arquivos e a rede. O ASP.NET Core suporta testes de integração usando uma estrutura de teste de unidade com um host de teste e um servidor de teste na memória.

Este artigo pressupõe uma compreensão básica de testes de unidade. Se não estiver familiarizado com conceitos de teste, consulte o artigo Teste de Unidade no .NET Core e no .NET Standard e seu conteúdo associado.

Visualizar ou baixar código de exemplo (como transferir)

A aplicação de exemplo é uma aplicação Razor Páginas e pressupõe um conhecimento básico de Razor Páginas. Se não estiver familiarizado com o Razor Pages, consulte os seguintes artigos:

Para testar SPAs, recomendamos uma ferramenta como Playwright for .NET, que pode automatizar um navegador.

Introdução aos testes de integração

Os testes de integração avaliam os componentes de um aplicativo em um nível mais amplo do que testes de unidade. Os testes de unidade são usados para testar componentes de software isolados, como métodos de classe individuais. Os testes de integração confirmam que dois ou mais componentes do aplicativo trabalham juntos para produzir um resultado esperado, possivelmente incluindo todos os componentes necessários para processar totalmente uma solicitação.

Esses testes mais amplos são usados para testar a infraestrutura e toda a estrutura do aplicativo, geralmente incluindo os seguintes componentes:

  • Base de dados
  • Sistema de ficheiros
  • Dispositivos de rede
  • Pipeline de solicitação-resposta

Os testes de unidade usam componentes fabricados, conhecidos como falsos ou objetos falsificados, no lugar de componentes de infraestrutura.

Em contraste com os testes de unidade, os testes de integração:

  • Use os componentes reais que o aplicativo usa na produção.
  • Exigem mais código e processamento de dados.
  • Leve mais tempo para correr.

Portanto, limite o uso de testes de integração aos cenários de infraestrutura mais importantes. Se um comportamento puder ser testado usando um teste de unidade ou um teste de integração, escolha o teste de unidade.

Em discussões de testes de integração, o projeto testado é frequentemente chamado de System Under Test, ou "SUT" para abreviar. "SUT" é usado ao longo deste artigo para se referir ao aplicativo ASP.NET Core que está sendo testado.

Não escreva testes de integração para cada de permutação de dados e acesso a arquivos com bancos de dados e sistemas de arquivos. Independentemente de quantos lugares em um aplicativo interagem com bancos de dados e sistemas de arquivos, um conjunto focado de testes de integração de leitura, gravação, atualização e exclusão geralmente é capaz de testar adequadamente os componentes do banco de dados e do sistema de arquivos. Use testes de unidade para testes de rotina de lógica de método que interagem com esses componentes. Em testes de unidade, o uso de falsificações ou simulações de infraestrutura resulta em execução de teste mais rápida.

ASP.NET Testes de integração principais

Os testes de integração no ASP.NET Core requerem o seguinte:

  • Um projeto de teste é usado para conter e executar os testes. O projeto de teste tem uma referência ao SUT.
  • O projeto de teste cria um host de teste para o SUT e usa um cliente de servidor de teste para lidar com solicitações e respostas com o SUT.
  • Um corredor de teste é usado para executar os testes e relatar os resultados do teste.

Os testes de integração seguem uma sequência de eventos que incluem as etapas usuais de teste Preparar, Agire Asserir:

  1. A hospedagem web da SUT está configurada.
  2. Um cliente de servidor de teste é criado para enviar solicitações ao aplicativo.
  3. A etapa de teste Organizar é executada: o aplicativo de teste prepara uma solicitação.
  4. A etapa de teste do Act é executada: o cliente envia a solicitação e recebe a resposta.
  5. A etapa de teste Assert é executada: A resposta real é validada como uma de aprovação ou falha com base em uma resposta esperada.
  6. O processo continua até que todos os testes sejam executados.
  7. Os resultados dos testes são divulgados.

Normalmente, o host da Web de teste é configurado de forma diferente do host normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usados para os testes.

Os componentes de infraestrutura, como o host da Web de teste e o servidor de teste na memória (TestServer), são fornecidos ou gerenciados pelo pacote Microsoft.AspNetCore.Mvc.Testing. O uso deste pacote simplifica a criação e execução de testes.

O pacote Microsoft.AspNetCore.Mvc.Testing lida com as seguintes tarefas:

  • Copia o arquivo de dependências (.deps) do SUT para o diretório bin do projeto de teste.
  • Define a raiz de conteúdo como a raiz do projeto do SUT para que arquivos estáticos e páginas/visualizações sejam encontrados quando os testes forem executados.
  • Fornece a classe WebApplicationFactory, para simplificar o processo de configuração do SUT com TestServer.

A documentação de testes de unidade descreve como configurar um projeto de teste e uma ferramenta de execução de testes, juntamente com instruções detalhadas sobre como executar testes e recomendações sobre como nomear testes e classes de teste.

Separe os testes de unidade dos testes de integração em diferentes projetos. Separar os testes:

  • Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
  • Permite controlar qual conjunto de testes é executado.

Não há praticamente nenhuma diferença entre a configuração para testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença está na forma como os testes são nomeados. Em uma aplicação Razor Pages, os testes de pontos de extremidade de página geralmente são baseados no nome da classe do modelo de página (por exemplo, IndexPageTests para testar a integração de componentes na página Índice). Em um aplicativo MVC, os testes geralmente são organizados por classes de controlador e nomeados após os controladores que eles testam (por exemplo, HomeControllerTests para testar a integração de componentes para o controlador Home).

Pré-requisitos do aplicativo de teste

O projeto de ensaio deve:

Esses pré-requisitos podem ser vistos no aplicativo de exemplo . Inspecione o arquivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. A aplicação de exemplo usa a estrutura de teste xUnit e a biblioteca do analisador AngleSharp, portanto, a aplicação de exemplo também faz referência:

Em aplicativos que usam xunit.runner.visualstudio versão 2.4.2 ou posterior, o projeto de teste deve fazer referência ao pacote Microsoft.NET.Test.Sdk.

O Entity Framework Core também é usado nos testes. Consulte o arquivo de projeto no GitHub.

Ambiente SUT

Se o ambiente do do SUT não estiver definido, o ambiente padrão será Desenvolvimento.

Testes básicos com o padrão WebApplicationFactory

Exponha a classe Program implicitamente definida para o projeto de teste seguindo um destes procedimentos:

  • Exponha tipos internos do aplicativo Web para o projeto de teste. Isso pode ser feito no arquivo do projeto SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Torne a classe Program pública usando uma declaração parcial de classe:

    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    

    A aplicação de exemplo usa a abordagem de classe parcial Program.

WebApplicationFactory<TEntryPoint> é usado para criar um TestServer para os testes de integração. TEntryPoint é a classe de ponto de entrada do SUT, geralmente Program.cs.

As classes de teste implementam uma interface de fixação de classe (IClassFixture) para indicar que a classe contém testes e fornecer instâncias de objetos partilhados entre os testes na classe.

A seguinte classe de teste, BasicTests, usa o WebApplicationFactory para inicializar o SUT e providenciar um HttpClient a um método de teste, Get_EndpointsReturnSuccessAndCorrectContentType. O método verifica se o código de status da resposta foi bem-sucedido (200-299) e o cabeçalho Content-Type está text/html; charset=utf-8 para várias páginas do aplicativo.

CreateClient() cria uma instância de HttpClient que segue automaticamente redirecionamentos e lida com cookies.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

Por padrão, os cookies não essenciais não são preservados nas solicitações quando a política de consentimento do Regulamento Geral de Proteção de Dados está ativada. Para preservar cookies não essenciais, como os usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Cookies essenciais.

AngleSharp vs Application Parts para verificações antifalsificação

Este artigo usa o analisador AngleSharp para lidar com as verificações antifalsificação carregando páginas e analisando o HTML. Para testar os pontos de extremidade das visualizações do controlador e do Razor Pages em um nível inferior, sem se preocupar com a forma como eles são renderizados no navegador, considere usar Application Parts. A abordagem Application Parts injeta um controlador ou Razor Page no aplicativo que pode ser usado para fazer solicitações JSON para obter os valores necessários. Para obter mais informações, consulte o blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts e de repositório GitHub associado pelo Martin Costello.

Personalizar WebApplicationFactory

É possível criar a configuração do host da Web de forma independente das classes de teste, por meio da herança de WebApplicationFactory<TEntryPoint> para criar uma ou mais fábricas personalizadas.

  1. Herdar de WebApplicationFactory e substituir ConfigureWebHost. O IWebHostBuilder permite a configuração da coleção de serviços com IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType == 
                        typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    A propagação do banco de dados no aplicativo de exemplo é executada pelo método InitializeDbForTests. O método é descrito no exemplo de testes de integração na seção : Organização da aplicação de teste.

    O contexto da base de dados da SUT está registado em Program.cs. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois de o código Program.cs do aplicativo é executado. Para usar um banco de dados diferente nos testes em vez do banco de dados do aplicativo, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices.

    O aplicativo de exemplo localiza o descritor de serviço para o contexto do banco de dados e usa o descritor para remover o registro de serviço. Em seguida, a fábrica adiciona um novo ApplicationDbContext que usa um banco de dados na memória para os testes.

    Para se conectar a um banco de dados diferente, altere o DbConnection. Para usar um banco de dados de teste do SQL Server:

  1. Utilize o CustomWebApplicationFactory personalizado em classes de teste. O exemplo a seguir usa a fábrica na classe IndexPageTests:

    public class IndexPageTests :
        IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program>
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<Program> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    

    O cliente do aplicativo de exemplo está configurado para impedir que o HttpClient siga redirecionamentos. Conforme explicado mais adiante na seção de autenticação simulada, isso permite que os testes verifiquem o resultado da primeira resposta do aplicativo. A primeira resposta é um redirecionamento em muitos desses testes com um cabeçalho Location.

  2. Um teste típico usa os métodos HttpClient e auxiliar para processar a solicitação e a resposta:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Qualquer solicitação POST ao SUT deve passar pela verificação de antifalsificação feita automaticamente pelo sistema de proteção de dados contra falsificação do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:

  1. Faça um pedido para a página.
  2. Analise o cookie antifalsificação e solicite o token de validação da resposta.
  3. Faça a solicitação POST com o antifalsificação cookie e solicite o token de validação em vigor.

Os métodos de extensão auxiliar SendAsync (Helpers/HttpClientExtensions.cs) e o método auxiliar GetDocumentAsync (Helpers/HtmlHelpers.cs) na aplicação de exemplo usam o analisador AngleSharp para manipular a verificação antifalsificação com os seguintes métodos:

  • GetDocumentAsync: Recebe o HttpResponseMessage e devolve um IHtmlDocument. GetDocumentAsync usa uma fábrica que prepara uma resposta virtual com base no HttpResponseMessageoriginal. Para obter mais informações, consulte a documentação do AngleSharp.
  • SendAsync métodos de extensão para o HttpClient compõem um HttpRequestMessage e chamam SendAsync(HttpRequestMessage) para enviar solicitações ao SUT. Sobrecargas para SendAsync aceitam o formulário HTML (IHtmlFormElement) e o seguinte:
    • Botão Enviar do formulário (IHtmlElement)
    • Coleção de valores de formulário (IEnumerable<KeyValuePair<string, string>>)
    • Botão Enviar (IHtmlElement) e valores de formulário (IEnumerable<KeyValuePair<string, string>>)

AngleSharp é uma biblioteca de análises sintáticas de terceiros usada para fins de demonstração neste artigo e na aplicação de exemplo. O AngleSharp não é suportado ou necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o Html Agility Pack (HAP). Outra abordagem é escrever código para lidar com o token de verificação de solicitação do sistema antifalsificação e o antifalsificação cookie diretamente. Consulte AngleSharp vs Application Parts para verificações antifalsificação neste artigo para obter mais informações.

O EF-Core provedor de banco de dados na memória pode ser usado para testes limitados e básicos, no entanto, o provedor SQLite é a escolha recomendada para testes na memória.

Consulte Estender a Inicialização com Filtros de Inicialização, que mostra como configurar middleware usando IStartupFilter, o que é útil quando um teste requer um serviço ou middleware personalizado.

Personalize o cliente com WithWebHostBuilder

Quando uma configuração adicional é necessária dentro de um método de teste, WithWebHostBuilder cria um novo WebApplicationFactory com um IWebHostBuilder que é ainda mais personalizado pela configuração.

O código de exemplo chama WithWebHostBuilder para substituir serviços configurados por stubs de teste. Para obter mais informações e exemplos de uso, consulte de serviços simulados de injeção neste artigo.

O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot do aplicativo de exemplo demonstra o uso de WithWebHostBuilder. Este teste executa uma exclusão de registro no banco de dados acionando um envio de formulário no SUT.

Como outro teste na classe IndexPageTests executa uma operação que exclui todos os registros no banco de dados e pode ser executado antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot, o banco de dados é repropagado nesse método de teste para garantir que um registro esteja presente para o SUT excluir. A seleção do primeiro botão de exclusão do formulário messages no SUT é simulada na solicitação ao SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Opções do cliente

Consulte a página WebApplicationFactoryClientOptions para obter os padrões e as opções disponíveis ao criar instâncias HttpClient.

Crie a classe WebApplicationFactoryClientOptions e passe-a para o método CreateClient():

public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

NOTA: Para evitar avisos de redirecionamento HTTPS em logs ao usar o middleware de redirecionamento HTTPS, defina BaseAddress = new Uri("https://localhost")

Injetar serviços simulados

Os serviços podem ser substituídos em um teste com uma chamada para ConfigureTestServices no construtor de hosts. Para definir o escopo dos serviços substituídos para o teste em si, o método WithWebHostBuilder é usado para recuperar um host builder. Isso pode ser visto nos seguintes testes:

O SUT de exemplo inclui um serviço com um escopo definido que retorna uma citação. A cotação é incorporada em um campo oculto na página Índice quando a página Índice é solicitada.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Program.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

A seguinte marcação é gerada quando o aplicativo SUT é executado:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Para testar o serviço e a injeção de cotação em um teste de integração, um serviço simulado é injetado no SUT pelo teste. O serviço simulado substitui o QuoteService do aplicativo por um serviço fornecido pelo aplicativo de teste, chamado TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

Quando o ConfigureTestServices é chamado, o serviço com escopo é registado.

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

A marcação gerada durante a execução do teste reflete o texto da citação citado por TestQuoteService, portanto, a asserção é bem-sucedida.

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Autenticação simulada

Os testes na classe AuthTests verificam se um endpoint seguro:

  • Redireciona um usuário não autenticado para a página de login do aplicativo.
  • Retorna conteúdo para um usuário autenticado.

No SUT, a página /SecurePage utiliza uma convenção denominada AuthorizePage para aplicar um elemento AuthorizeFilter à página. Para obter mais informações, consulte as convenções de autorização das páginas Razor.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

No teste de Get_SecurePageRedirectsAnUnauthenticatedUser, um WebApplicationFactoryClientOptions é definido para não permitir redirecionamentos definindo AllowAutoRedirect como false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Ao não permitir que o cliente siga o redirecionamento, as seguintes verificações podem ser feitas:

  • O código de status retornado pelo SUT pode ser verificado em relação ao resultado HttpStatusCode.Redirect esperado, não o código de status final após o redirecionamento para a página de login, que seria HttpStatusCode.OK.
  • O valor do cabeçalho Location nos cabeçalhos de resposta é verificado para confirmar que começa com http://localhost/Identity/Account/Login, e não com a resposta final da página de início de sessão, onde o cabeçalho Location não estaria presente.

O aplicativo de teste pode simular um AuthenticationHandler<TOptions> em ConfigureTestServices para testar aspetos de autenticação e autorização. Um cenário mínimo retorna AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

O TestAuthHandler é chamado para autenticar um usuário quando o esquema de autenticação é definido como TestScheme onde AddAuthentication está registrado para ConfigureTestServices. É importante que o esquema de TestScheme corresponda ao esquema esperado pelo seu aplicativo. Caso contrário, a autenticação não funcionará.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Para obter mais informações sobre WebApplicationFactoryClientOptions, consulte a seção opções do cliente.

Testes básicos para middleware de autenticação

Consulte este repositório no GitHub para testes básicos de middleware de autenticação. Ele contém um servidor de teste que é específico para o cenário de teste.

Definir o ambiente

Defina o ambiente na fábrica de aplicações personalizadas:

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType == 
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Como a infraestrutura de teste infere o caminho raiz do conteúdo do aplicativo

O construtor WebApplicationFactory infere o caminho da raiz de conteúdo da aplicação procurando um WebApplicationFactoryContentRootAttribute no assembly que contém os testes de integração com uma chave igual ao assembly TEntryPointSystem.Reflection.Assembly.FullName. Caso um atributo com a chave correta não seja encontrado, WebApplicationFactory volta a procurar por um arquivo de solução (.sln) e acrescenta o nome do assembly TEntryPoint ao diretório da solução. O diretório raiz do aplicativo (o caminho da raiz do conteúdo) é usado para descobrir exibições e arquivos de conteúdo.

Desativar cópia de sombra

A cópia de sombra faz com que os testes sejam executados em um diretório diferente do diretório de saída. Se os testes dependerem do carregamento de arquivos relativos ao Assembly.Location e você encontrar problemas, talvez seja necessário desabilitar a cópia de sombra.

Para desativar a cópia de sombras ao usar xUnit, crie um arquivo de xunit.runner.json no diretório do projeto de teste, com a configuração correta .

{
  "shadowCopy": false
}

Eliminação de objetos

Depois que os testes da implementação do IClassFixture são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory. Se os objetos instanciados pelo desenvolvedor precisarem ser eliminados, elimine-os na implementação IClassFixture. Para obter mais informações, consulte Implementing a Dispose method.

Exemplo de testes de integração

O de aplicativo de exemplo é composto por dois aplicativos:

Aplicação Diretório de projetos Descrição
Aplicativo de mensagem (o SUT) src/RazorPagesProject Permite que um usuário adicione, exclua um, exclua todos e analise mensagens.
Aplicativo de teste tests/RazorPagesProject.Tests Usado para testar a integração do SUT.

Os testes podem ser executados usando os recursos de teste internos de um IDE, como Visual Studio. Se estiver usando Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando no diretório tests/RazorPagesProject.Tests:

dotnet test

Organização do aplicativo de mensagem (SUT)

O SUT é um sistema de mensagens Razor Pages com as seguintes características:

  • A página Índice do aplicativo (Pages/Index.cshtml e Pages/Index.cshtml.cs) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (média de palavras por mensagem).
  • Uma mensagem é descrita pela classe Message (Data/Message.cs) com duas propriedades: Id (chave) e Text (mensagem). A propriedade Text é obrigatória e limitada a 200 caracteres.
  • As mensagens são armazenadas usando o banco de dados na memória do Entity Framework †.
  • O aplicativo contém uma camada de acesso a dados (DAL) em sua classe de contexto de banco de dados, AppDbContext (Data/AppDbContext.cs).
  • Se o banco de dados estiver vazio na inicialização do aplicativo, o armazenamento de mensagens será inicializado com três mensagens.
  • O aplicativo inclui um /SecurePage que só pode ser acessado por um usuário autenticado.

†O artigo do EF, Test with InMemory, explica como usar um banco de dados na memória para testes com o MSTest. Este tópico usa o xUnit estrutura de teste. Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.

Embora o aplicativo não use o padrão de repositório e não seja um exemplo eficaz do padrão de Unidade de Trabalho (UoW), o Razor Pages oferece suporte a esses padrões de desenvolvimento. Para mais informações, consulte Conceção da camada de persistência da infraestrutura e Lógica do controlador de testes (o exemplo implementa o padrão de repositório).

Testar a organização do aplicativo

O aplicativo de teste é um aplicativo de console dentro do diretório tests/RazorPagesProject.Tests.

Diretório de aplicativos de teste Descrição
AuthTests Contém métodos de teste para:
  • Aceder a uma página segura por um utilizador não autenticado.
  • Aceder a uma página segura por um utilizador autenticado com uma simulação AuthenticationHandler<TOptions>.
  • Obter um perfil de usuário do GitHub e verificar o login de usuário do perfil.
BasicTests Contém um método de teste para roteamento e tipo de conteúdo.
IntegrationTests Contém os testes de integração para a página Índice usando a classe WebApplicationFactory personalizada.
Helpers/Utilities
  • Utilities.cs contém o método InitializeDbForTests usado para semear o banco de dados com dados de teste.
  • O HtmlHelpers.cs fornece um método para retornar um AngleSharp IHtmlDocument para uso nos métodos de teste.
  • HttpClientExtensions.cs fornece sobrecargas para SendAsync enviar solicitações ao SUT.

A estrutura de teste é xUnit. Os testes de integração são realizados usando o Microsoft.AspNetCore.TestHost, que inclui o TestServer. Como o pacote Microsoft.AspNetCore.Mvc.Testing é usado para configurar o host de teste e o servidor de teste, os pacotes TestHost e TestServer não exigem referências diretas de pacote no arquivo de projeto do aplicativo de teste ou na configuração do desenvolvedor no aplicativo de teste.

Os testes de integração geralmente exigem um pequeno conjunto de dados no banco de dados antes da execução do teste. Por exemplo, um teste de exclusão chama para uma exclusão de registro de banco de dados, portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão seja bem-sucedida.

O aplicativo de exemplo semeia o banco de dados com três mensagens em Utilities.cs que os testes podem usar quando são executados:

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

O contexto da base de dados da SUT está registado em Program.cs. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois de o código Program.cs do aplicativo é executado. Para usar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices. Para mais informações, consulte a seção Personalizar WebApplicationFactory .

Recursos adicionais

Este tópico pressupõe uma compreensão básica de testes de unidade. Se não estiver familiarizado com conceitos de teste, consulte o tópico Teste de unidade de no .NET Core e no .NET Standard e seu conteúdo vinculado.

Ver ou baixar código de exemplo (como fazer a transferência)

A aplicação de exemplo é uma aplicação Razor Pages e exige um conhecimento básico de Razor Pages. Se não estiver familiarizado com o Razor Pages, consulte os seguintes tópicos:

Observação

Para testar SPAs, recomendamos uma ferramenta como o Playwright for .NET, que pode automatizar um navegador.

Introdução aos testes de integração

Os testes de integração avaliam os componentes de uma aplicação num nível mais amplo do que testes de unidade. Os testes de unidade são usados para testar componentes de software isolados, como métodos de classe individuais. Os testes de integração confirmam que dois ou mais componentes do aplicativo trabalham juntos para produzir um resultado esperado, possivelmente incluindo todos os componentes necessários para processar totalmente uma solicitação.

Esses testes mais amplos são usados para testar a infraestrutura e toda a estrutura do aplicativo, geralmente incluindo os seguintes componentes:

  • Base de dados
  • Sistema de ficheiros
  • Dispositivos de rede
  • Pipeline de resposta a pedidos

Os testes de unidade usam componentes fabricados, conhecidos como falsos ou objetos de simulação, no lugar de componentes de infraestrutura.

Em contraste com os testes de unidade, os testes de integração:

  • Use os componentes reais que o aplicativo usa na produção.
  • Exigem mais código e processamento de dados.
  • Leve mais tempo para correr.

Portanto, limite o uso de testes de integração aos cenários de infraestrutura mais importantes. Se um comportamento puder ser testado usando um teste de unidade ou um teste de integração, escolha o teste de unidade.

Em discussões de testes de integração, o projeto testado é frequentemente chamado de System Under Test, ou "SUT" para abreviar. "SUT" é usado ao longo deste artigo para se referir ao aplicativo ASP.NET Core que está sendo testado.

Não escreva testes de integração para cada de permutação de dados e acesso a arquivos com bancos de dados e sistemas de arquivos. Independentemente de quantos lugares em um aplicativo interagem com bancos de dados e sistemas de arquivos, um conjunto focado de testes de integração de leitura, gravação, atualização e exclusão geralmente é capaz de testar adequadamente os componentes do banco de dados e do sistema de arquivos. Use testes de unidade para testes de rotina de lógica de método que interagem com esses componentes. Em testes de unidade, o uso de falsificações ou simulações de infraestrutura resulta em execução de teste mais rápida.

ASP.NET Testes de integração principais

Os testes de integração no ASP.NET Core requerem o seguinte:

  • Um projeto de teste é usado para conter e executar os testes. O projeto de teste tem uma referência ao SUT.
  • O projeto de teste cria um host de teste para o SUT e usa um cliente de servidor de teste para lidar com solicitações e respostas com o SUT.
  • Um corredor de teste é usado para executar os testes e relatar os resultados do teste.

Os testes de integração seguem uma sequência de eventos que incluem as etapas usuais de teste Preparar, Executare Verificar:

  1. O anfitrião web da SUT está configurado.
  2. Um cliente de servidor de teste é criado para enviar solicitações ao aplicativo.
  3. A etapa de teste Arranjar é executada: a aplicação de teste prepara uma solicitação.
  4. A etapa de teste do Act é executada: o cliente envia a solicitação e recebe a resposta.
  5. A etapa de teste Assert é executada: A resposta real é validada como um êxito ou falha com base em uma resposta esperada.
  6. O processo continua até que todos os testes sejam executados.
  7. Os resultados dos testes foram divulgados.

Normalmente, o host da Web de teste é configurado de forma diferente do host normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usados para os testes.

Os componentes de infraestrutura, como o host da Web de teste e o servidor de teste na memória (TestServer), são fornecidos ou gerenciados pelo pacote de Microsoft.AspNetCore.Mvc.Testing. O uso deste pacote simplifica a criação e execução de testes.

O pacote Microsoft.AspNetCore.Mvc.Testing lida com as seguintes tarefas:

  • Copia o arquivo de dependências (.deps) do SUT para o diretório bin do projeto de teste.
  • Define a raiz de conteúdo para a raiz do projeto SUT para que arquivos estáticos e páginas/visualizações sejam encontrados quando os testes forem executados.
  • Fornece a classe WebApplicationFactory para agilizar a inicialização do SUT com TestServer.

A documentação dos testes de unidade descreve como configurar um projeto de teste e um executante de testes, juntamente com instruções detalhadas sobre como executar testes e recomendações sobre como nomear testes e classes de teste.

Separe os testes de unidade dos testes de integração em diferentes projetos. Separar os testes:

  • Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
  • Permite controlar qual conjunto de testes é executado.

Não há praticamente nenhuma diferença entre a configuração para testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença está na forma como os testes são nomeados. Em um aplicativo Razor Pages, os testes de pontos de extremidade de página são geralmente nomeados de acordo com a classe de modelo de página (por exemplo, IndexPageTests para testar a integração de componentes para página Índice). Em um aplicativo MVC, os testes geralmente são organizados por classes de controlador e nomeados após os controladores que eles testam (por exemplo, HomeControllerTests para testar a integração de componentes para o controlador Home).

Pré-requisitos do aplicativo de teste

O projeto de ensaio deve:

Esses pré-requisitos podem ser vistos no aplicativo de exemplo . Inspecione o arquivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. O aplicativo de exemplo usa a estrutura de teste xUnit e a biblioteca de analisador AngleSharp, portanto, o aplicativo de exemplo também faz referência:

Em aplicativos que usam xunit.runner.visualstudio versão 2.4.2 ou posterior, o projeto de teste deve fazer referência ao pacote Microsoft.NET.Test.Sdk.

O Entity Framework Core também é usado nos testes. As referências do aplicativo:

Ambiente SUT

Se a de ambiente do SUT não estiver definida, o padrão do ambiente será Desenvolvimento.

Testes básicos com o padrão WebApplicationFactory

WebApplicationFactory<TEntryPoint> é usado para criar um TestServer para os testes de integração. TEntryPoint é a classe de ponto de entrada do SUT, geralmente a classe Startup.

As classes de teste implementam uma interface de de fixação de classe (IClassFixture) para indicar que a classe contém testes e fornecer instâncias de objeto compartilhado entre os testes na classe.

A seguinte classe de teste, BasicTests, utiliza o WebApplicationFactory para inicializar o SUT e fornecer um HttpClient a um método de teste, Get_EndpointsReturnSuccessAndCorrectContentType. O método verifica se o código de status da resposta foi bem-sucedido (códigos de status no intervalo 200-299) e se o cabeçalho Content-Type está text/html; charset=utf-8 para várias páginas do aplicativo.

CreateClient() cria uma instância de HttpClient que segue automaticamente redirecionamentos e lida com cookies.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
    private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;

    public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

Por padrão, os cookies não essenciais não são preservados em todas as solicitações quando a política de consentimento GDPR está ativada. Para preservar cookies não essenciais, como os usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Cookies Essenciais.

Personalizar WebApplicationFactory

A configuração do anfitrião Web pode ser criada de forma independente das classes de teste, herdando de WebApplicationFactory para criar uma ou mais fábricas personalizadas.

  1. Herdar de WebApplicationFactory e substituir ConfigureWebHost. O IWebHostBuilder permite a configuração da coleção de serviços com ConfigureServices:

    public class CustomWebApplicationFactory<TStartup>
        : WebApplicationFactory<TStartup> where TStartup: class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(descriptor);
    
                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });
    
                var sp = services.BuildServiceProvider();
    
                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
    
                    db.Database.EnsureCreated();
    
                    try
                    {
                        Utilities.InitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding the " +
                            "database with test messages. Error: {Message}", ex.Message);
                    }
                }
            });
        }
    }
    

    A propagação do banco de dados no aplicativo de exemplo é executada pelo método InitializeDbForTests. O método é descrito na seção Integration tests sample: Test app organization seção.

    O contexto da base de dados do SUT é registado no seu método Startup.ConfigureServices. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois de o código Startup.ConfigureServices do aplicativo é executado. A ordem de execução é uma alteração de quebra para o Generic Host com o lançamento do ASP.NET Core 3.0. Para usar um banco de dados diferente do banco de dados do aplicativo para os testes, o contexto deste deve ser substituído em builder.ConfigureServices.

    Para SUTs que ainda usam o Web Host, o retorno de chamada builder.ConfigureServices do aplicativo de teste é executado antes de o código Startup.ConfigureServices do SUT. O callback builder.ConfigureTestServices do aplicativo de teste é executado após.

    O aplicativo de exemplo localiza o descritor de serviço para o contexto do banco de dados e usa o descritor para remover o registro de serviço. Em seguida, a fábrica adiciona um novo ApplicationDbContext que usa um banco de dados na memória para os testes.

    Para se conectar a um banco de dados diferente do banco de dados na memória, altere a chamada UseInMemoryDatabase para conectar o contexto a um banco de dados diferente. Para usar um banco de dados de teste do SQL Server:

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. Use o CustomWebApplicationFactory personalizado em classes de teste. O exemplo a seguir usa a fábrica na classe IndexPageTests:

    public class IndexPageTests : 
        IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> 
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
                {
                    AllowAutoRedirect = false
                });
        }
    

    O cliente do aplicativo de exemplo está configurado para impedir que o HttpClient siga redirecionamentos. Conforme explicado mais adiante na seção de autenticação simulada, isso permite que os testes verifiquem o resultado da primeira resposta do aplicativo. A primeira resposta é um redirecionamento em muitos desses testes com um cabeçalho Location.

  3. Um teste típico usa os métodos HttpClient e auxiliar para processar a solicitação e a resposta:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Qualquer solicitação POST ao SUT deve satisfazer a verificação antifalsificação feita automaticamente pelo sistema antifalsificação de proteção de dados do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:

  1. Faça um pedido para a página.
  2. Analise o cookie antifalsificação e solicite o token de validação da resposta.
  3. Faça a solicitação POST com o antifalsificação cookie e com o token de validação de solicitações devidamente posicionado.

Os métodos de extensão auxiliar SendAsync (Helpers/HttpClientExtensions.cs) e o método auxiliar de GetDocumentAsync (Helpers/HtmlHelpers.cs) no aplicativo de exemplo usam o analisador de AngleSharp para manipular a verificação antifalsificação com os seguintes métodos:

  • GetDocumentAsync: Recebe o HttpResponseMessage e devolve um IHtmlDocument. GetDocumentAsync usa uma fábrica que prepara uma resposta virtual com base no HttpResponseMessageoriginal. Para obter mais informações, consulte a documentação do AngleSharp.
  • SendAsync métodos de extensão para o HttpClient compõem um HttpRequestMessage e chamam SendAsync(HttpRequestMessage) para enviar solicitações ao SUT. As sobrecargas de SendAsync aceitam o formulário HTML (IHtmlFormElement) e o seguinte:
    • Botão Enviar do formulário (IHtmlElement)
    • Coleção de valores de formulário (IEnumerable<KeyValuePair<string, string>>)
    • Botão Enviar (IHtmlElement) e valores de formulário (IEnumerable<KeyValuePair<string, string>>)

Observação

AngleSharp é uma biblioteca de análise de terceiros usada para fins de demonstração neste tópico e no aplicativo de exemplo. O AngleSharp não é suportado ou necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o Html Agility Pack (HAP). Outra abordagem é escrever código para lidar diretamente com o token de verificação de solicitação do sistema antifalsificação e o token antifalsificação cookie.

Observação

O provedor de banco de dados na memória EF-Core pode ser usado para testes limitados e básicos, no entanto, o provedor SQLite é a escolha recomendada para testes em memória.

Personalize o cliente com WithWebHostBuilder

Quando uma configuração adicional é necessária dentro de um método de teste, WithWebHostBuilder cria um novo WebApplicationFactory com um IWebHostBuilder que é ainda mais personalizado pela configuração.

O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot do aplicativo de exemplo demonstra o uso de WithWebHostBuilder. Este teste executa uma exclusão de registro no banco de dados acionando um envio de formulário no SUT.

Como outro teste na classe IndexPageTests executa uma operação que exclui todos os registros no banco de dados e pode ser executado antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot, o banco de dados é repropagado nesse método de teste para garantir que um registro esteja presente para o SUT excluir. A seleção do primeiro botão de exclusão do formulário messages no SUT é simulada na solicitação ao SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                var serviceProvider = services.BuildServiceProvider();

                using (var scope = serviceProvider.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices
                        .GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<IndexPageTests>>();

                    try
                    {
                        Utilities.ReinitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding " +
                            "the database with test messages. Error: {Message}", 
                            ex.Message);
                    }
                }
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Opções do cliente

A tabela a seguir mostra o WebApplicationFactoryClientOptions padrão disponível ao criar instâncias HttpClient.

Opção Descrição Padrão
AllowAutoRedirect Obtém ou define se as instâncias HttpClient devem ou não seguir automaticamente as respostas de redirecionamento. true
BaseAddress Obtém ou define o endereço base das instâncias de HttpClient. http://localhost
HandleCookies Obtém ou define se HttpClient instâncias devem lidar com cookies. true
MaxAutomaticRedirections Obtém ou define o número máximo de respostas de redirecionamento que HttpClient instâncias devem seguir. 7

Crie a classe WebApplicationFactoryClientOptions e passe-a para o método CreateClient() (os valores padrão são mostrados no exemplo de código):

// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;

_client = _factory.CreateClient(clientOptions);

Injetar serviços simulados

Os serviços podem ser substituídos em um teste com uma chamada para ConfigureTestServices no construtor de hosts. Para injetar serviços simulados, o SUT deve ter uma classe Startup com um método Startup.ConfigureServices.

O SUT de exemplo inclui um serviço com escopo definido que retorna uma cotação. A citação é incorporada em um campo escondido na página de índice quando a página de índice é solicitada.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Startup.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

A seguinte marcação é gerada quando o aplicativo SUT é executado:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Para testar o serviço e a injeção de cotação em um teste de integração, um serviço simulado é injetado no SUT pelo teste. O serviço simulado substitui o QuoteService do aplicativo por um serviço fornecido pelo aplicativo de teste, chamado TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices é chamado e o serviço com escopo é registrado:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

A marcação produzida durante a execução do teste reflete o texto da citação fornecido por TestQuoteService; assim, a asserção é bem-sucedida.

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Autenticação simulada

Os testes na classe AuthTests verificam se um endpoint seguro:

  • Redireciona um usuário não autenticado para a página de login do aplicativo.
  • Retorna conteúdo para um usuário autenticado.

No SUT, a página /SecurePage usa uma convenção AuthorizePage para aplicar um AuthorizeFilter à página. Para mais informações, consulte as Convenções de autorização das Páginas Razor.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

No teste de Get_SecurePageRedirectsAnUnauthenticatedUser, um WebApplicationFactoryClientOptions é definido para não permitir redirecionamentos definindo AllowAutoRedirect como false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login", 
        response.Headers.Location.OriginalString);
}

Ao não permitir que o cliente siga o redirecionamento, as seguintes verificações podem ser feitas:

  • O código de status retornado pelo SUT pode ser verificado em relação ao resultado HttpStatusCode.Redirect esperado, não o código de status final após o redirecionamento para a página de login, que seria HttpStatusCode.OK.
  • O valor do cabeçalho Location nos cabeçalhos de resposta é verificado para confirmar que ele começa com http://localhost/Identity/Account/Login, não com a resposta final da página de login, onde o cabeçalho Location não estaria presente.

O aplicativo de teste pode simular um AuthenticationHandler<TOptions> em ConfigureTestServices para testar aspetos de autenticação e autorização. Um cenário mínimo retorna um AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

O TestAuthHandler é chamado para autenticar um usuário quando o esquema de autenticação é definido como Test onde AddAuthentication está registrado para ConfigureTestServices. É importante que o esquema de Test corresponda ao esquema esperado pelo seu aplicativo. Caso contrário, a autenticação não funcionará.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication("Test")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "Test", options => {});
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Test");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Para obter mais informações sobre WebApplicationFactoryClientOptions, consulte a seção Opções do Cliente de.

Definir o ambiente

Por padrão, o ambiente de host e aplicativo do SUT é configurado para usar o ambiente de desenvolvimento. Para sobrescrever o ambiente do SUT quando usar IHostBuilder:

  • Defina a variável de ambiente ASPNETCORE_ENVIRONMENT (por exemplo, Staging, Productionou outro valor personalizado, como Testing).
  • Substitua CreateHostBuilder no aplicativo de teste para ler variáveis de ambiente prefixadas com ASPNETCORE.
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

Se o SUT usar o host da Web (IWebHostBuilder), substitua CreateWebHostBuilder:

protected override IWebHostBuilder CreateWebHostBuilder() =>
    base.CreateWebHostBuilder().UseEnvironment("Testing");

Como a infraestrutura de teste infere o caminho raiz do conteúdo do aplicativo

O construtor WebApplicationFactory infere o caminho da raiz de conteúdo da aplicação , procurando um WebApplicationFactoryContentRootAttribute no assembly que contém os testes de integração, com uma chave igual à do assembly TEntryPointSystem.Reflection.Assembly.FullName. Caso um atributo com a chave correta não seja encontrado, WebApplicationFactory reverte para procurar um arquivo de solução (.sln) e acrescenta o nome do assembly TEntryPoint ao diretório da solução. O diretório raiz do aplicativo (o caminho da raiz do conteúdo) é usado para descobrir exibições e arquivos de conteúdo.

Desativar cópia de sombra

A cópia de sombra faz com que os testes sejam executados em um diretório diferente do diretório de saída. Se os testes dependerem do carregamento de ficheiros relativos ao Assembly.Location e encontrardes problemas, talvez seja necessário desativar a cópia de sombra.

Para desabilitar a cópia de sombra ao usar xUnit, crie um arquivo de xunit.runner.json no diretório do projeto de teste, com a definição de configuração correta:

{
  "shadowCopy": false
}

Eliminação de objetos

Depois que os testes da implementação do IClassFixture são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory. Se os objetos instanciados pelo desenvolvedor precisarem ser eliminados, elimine-os na implementação IClassFixture. Para obter mais informações, consulte Implementing a Dispose method.

Exemplo de testes de integração

O de aplicativo de exemplo é composto por dois aplicativos:

Aplicação Diretório de projetos Descrição
Aplicativo de mensagem (o SUT) src/RazorPagesProject Permite que um usuário adicione, exclua um, exclua todos e analise mensagens.
Aplicativo de teste tests/RazorPagesProject.Tests Usado para testar a integração do SUT.

Os testes podem ser executados usando os recursos de teste internos de um IDE, como Visual Studio. Se estiver usando Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando no diretório tests/RazorPagesProject.Tests:

dotnet test

Organização do aplicativo de mensagem (SUT)

O SUT é um sistema de mensagens Razor Pages com as seguintes características:

  • A página Índice do aplicativo (Pages/Index.cshtml e Pages/Index.cshtml.cs) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (média de palavras por mensagem).
  • Uma mensagem é descrita pela classe Message (Data/Message.cs) com duas propriedades: Id (chave) e Text (mensagem). A propriedade Text é obrigatória e limitada a 200 caracteres.
  • As mensagens são armazenadas usando a base de dados em memória do Entity Framework †.
  • O aplicativo contém uma camada de acesso a dados (DAL) em sua classe de contexto de banco de dados, AppDbContext (Data/AppDbContext.cs).
  • Se o banco de dados estiver vazio na inicialização do aplicativo, o armazenamento de mensagens será inicializado com três mensagens.
  • O aplicativo inclui um /SecurePage que só pode ser acessado por um usuário autenticado.

†O tópico EF, Test with InMemory, explica como usar um banco de dados na memória para testes com MSTest. Este tópico usa a estrutura de teste xUnit . Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.

Embora o aplicativo não use o padrão de repositório e não seja um exemplo eficaz do padrão de Unidade de Trabalho (UoW), o Razor Pages oferece suporte a esses padrões de desenvolvimento. Para mais informações, consulte Planear a camada de persistência da infraestrutura e Testar a lógica do controlador (o exemplo implementa o padrão de repositório).

Testar a organização do aplicativo

O aplicativo de teste é um aplicativo de console dentro do diretório tests/RazorPagesProject.Tests.

Diretório de aplicativos de teste Descrição
AuthTests Contém métodos de teste para:
  • Aceder a uma página segura por um utilizador não autenticado.
  • Aceder a uma página segura por um utilizador autenticado com uma simulação AuthenticationHandler<TOptions>.
  • Obter um perfil de usuário do GitHub e verificar o login de usuário do perfil.
BasicTests Contém um método de teste para roteamento e tipo de conteúdo.
IntegrationTests Contém os testes de integração para a página Índice usando a classe WebApplicationFactory personalizada.
Helpers/Utilities
  • Utilities.cs contém o método InitializeDbForTests usado para semear o banco de dados com dados de teste.
  • HtmlHelpers.cs fornece um método para retornar um AngleSharp IHtmlDocument para ser usado pelos métodos de teste.
  • HttpClientExtensions.cs fornecem sobrecargas para SendAsync submeter pedidos ao SUT.

A estrutura de teste é xUnit. Os testes de integração são realizados usando o Microsoft.AspNetCore.TestHost, que inclui o TestServer. Como o pacote Microsoft.AspNetCore.Mvc.Testing é usado para configurar o host de teste e o servidor de teste, os pacotes TestHost e TestServer não exigem referências diretas de pacote no arquivo de projeto do aplicativo de teste ou na configuração do desenvolvedor no aplicativo de teste.

Os testes de integração geralmente exigem um pequeno conjunto de dados no banco de dados antes da execução do teste. Por exemplo, um teste de exclusão chama para uma exclusão de registro de banco de dados, portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão seja bem-sucedida.

O aplicativo de exemplo semeia o banco de dados com três mensagens em Utilities.cs que os testes podem usar quando são executados:

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

O contexto da base de dados do SUT é registado no seu método Startup.ConfigureServices. A função de retorno builder.ConfigureServices do aplicativo de teste é chamada depois de o código Startup.ConfigureServices do aplicativo ser executado. Para usar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices. Para obter mais informações, consulte a seção Personalizar WebApplicationFactory.

Para SUTs que ainda usam o Web Host, o retorno de chamada builder.ConfigureServices do aplicativo de teste é executado antes de o código Startup.ConfigureServices do SUT. O retorno de chamada builder.ConfigureTestServices do aplicativo de teste é executado após.

Recursos adicionais

Este artigo pressupõe uma compreensão básica de testes de unidade. Se não estiver familiarizado com conceitos de teste, consulte o artigo Teste de unidade de no .NET Core e no .NET Standard e seu conteúdo vinculado.

Exibir ou baixar código de exemplo (como descarregar)

A aplicação de exemplo é uma aplicação Razor Pages e assume uma compreensão básica de Razor Pages. Se não estiver familiarizado com o Razor Pages, consulte os seguintes artigos:

Para testar SPAs, recomendamos uma ferramenta como Playwright for .NET, que pode automatizar um navegador.

Introdução aos testes de integração

Os testes de integração avaliam os componentes de um aplicativo em um nível mais amplo do que testes de unidade. Os testes de unidade são usados para testar componentes de software isolados, como métodos de classe individuais. Os testes de integração confirmam que dois ou mais componentes do aplicativo trabalham juntos para produzir um resultado esperado, possivelmente incluindo todos os componentes necessários para processar totalmente uma solicitação.

Esses testes mais amplos são usados para testar a infraestrutura e toda a estrutura do aplicativo, geralmente incluindo os seguintes componentes:

  • Base de dados
  • Sistema de ficheiros
  • Dispositivos de rede
  • Pipeline de solicitação-resposta

Os testes de unidade usam componentes fabricados, conhecidos como objetos falsos ou objetos simulados, no lugar de componentes de infraestrutura.

Em contraste com os testes de unidade, os testes de integração:

  • Use os componentes reais que o aplicativo usa na produção.
  • Exigem mais código e processamento de dados.
  • Leve mais tempo para correr.

Portanto, limite o uso de testes de integração aos cenários de infraestrutura mais importantes. Se um comportamento puder ser testado usando um teste de unidade ou um teste de integração, escolha o teste de unidade.

Em discussões de testes de integração, o projeto testado é frequentemente chamado de System Under Test, ou "SUT" para abreviar. "SUT" é usado ao longo deste artigo para se referir ao aplicativo ASP.NET Core que está sendo testado.

Não escreva testes de integração para cada de permutação de dados e acesso a arquivos com bancos de dados e sistemas de arquivos. Independentemente de quantos lugares em um aplicativo interagem com bancos de dados e sistemas de arquivos, um conjunto focado de testes de integração de leitura, gravação, atualização e exclusão geralmente é capaz de testar adequadamente os componentes do banco de dados e do sistema de arquivos. Use testes de unidade para testes de rotina de lógica de método que interagem com esses componentes. Em testes de unidade, o uso de falsificações ou simulações de infraestrutura resulta em execução de teste mais rápida.

ASP.NET Testes de integração principais

Os testes de integração no ASP.NET Core requerem o seguinte:

  • Um projeto de teste é usado para conter e executar os testes. O projeto de teste tem uma referência ao SUT.
  • O projeto de teste cria um host de teste para o SUT e usa um cliente de servidor de teste para lidar com solicitações e respostas com o SUT.
  • Um corredor de teste é usado para executar os testes e relatar os resultados do teste.

Os testes de integração seguem uma sequência de eventos que inclui as etapas habituais de teste Preparação, Execuçãoe Verificação:

  1. O host da SUT está configurado.
  2. Um cliente de servidor de teste é criado para enviar solicitações ao aplicativo.
  3. A etapa de teste Organizar é executada: o aplicativo de teste prepara uma solicitação.
  4. A etapa de teste do Act é executada: o cliente envia a solicitação e recebe a resposta.
  5. A etapa de teste Assert é executada: A resposta real é validada como aprovação ou falha com base em uma resposta esperada.
  6. O processo continua até que todos os testes sejam executados.
  7. Os resultados dos testes são apresentados.

Normalmente, o host da Web de teste é configurado de forma diferente do host normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usados para os testes.

Os componentes de infraestrutura, como o host da Web de teste e o servidor de teste na memória (TestServer), são fornecidos ou gerenciados pelo pacote Microsoft.AspNetCore.Mvc.Testing . O uso deste pacote simplifica a criação e execução de testes.

O pacote Microsoft.AspNetCore.Mvc.Testing lida com as seguintes tarefas:

  • Copia o arquivo de dependências (.deps) do SUT para o diretório bin do projeto de teste.
  • Define a raiz de conteúdo para a raiz do projeto SUT para que arquivos estáticos e páginas/visualizações sejam encontrados quando os testes forem executados.
  • Fornece a classe WebApplicationFactory para agilizar a inicialização do SUT com TestServer.

A documentação dos testes de unidade descreve como configurar um projeto de teste e um executor de testes, juntamente com instruções detalhadas sobre como executar os testes e recomendações sobre como nomear testes e classes de teste.

Separe os testes de unidade dos testes de integração em diferentes projetos. Separar os testes:

  • Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
  • Permite controlar qual conjunto de testes é executado.

Não há praticamente nenhuma diferença entre a configuração para testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença está na forma como os testes são nomeados. Em uma aplicação Razor Pages, os testes dos endpoints de página geralmente são nomeados com base na classe de modelo de página (por exemplo, IndexPageTests para testar a integração de componentes na página Índice). Em um aplicativo MVC, os testes geralmente são organizados por classes de controlador e nomeados após os controladores que eles testam (por exemplo, HomeControllerTests para testar a integração de componentes para o controlador Home).

Pré-requisitos do aplicativo de teste

O projeto de ensaio deve:

Esses pré-requisitos podem ser vistos no aplicativo de exemplo . Inspecione o arquivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. O aplicativo de exemplo usa a estrutura de teste xUnit e a biblioteca de análise AngleSharp , o que significa que o aplicativo de exemplo também faz referência a:

Em aplicativos que usam xunit.runner.visualstudio versão 2.4.2 ou posterior, o projeto de teste deve fazer referência ao pacote Microsoft.NET.Test.Sdk.

O Entity Framework Core também é usado nos testes. Consulte o arquivo de projeto no GitHub.

Ambiente do SUT

Se o ambiente de do SUT não estiver definido, o ambiente padrão será Desenvolvimento.

Testes básicos com o padrão WebApplicationFactory

Exponha a classe Program implicitamente definida para o projeto de teste seguindo um destes procedimentos:

  • Exponha tipos internos da aplicação web ao projeto de teste. Isso pode ser feito no arquivo do projeto SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Torne a classe Program pública usando uma declaração parcial de classe:

    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    

    O aplicativo de exemplo usa a Program abordagem de classe parcial.

WebApplicationFactory<TEntryPoint> é usado para criar um TestServer para os testes de integração. TEntryPoint é a classe de ponto de entrada do SUT, geralmente Program.cs.

As classes de teste implementam uma interface de fixture de classe (IClassFixture) para indicar que a classe contém testes e fornecer instâncias de objeto compartilhado entre os testes na classe.

A seguinte classe de teste, BasicTests, utiliza o WebApplicationFactory para inicializar o SUT e fornecer um HttpClient a um método de teste, Get_EndpointsReturnSuccessAndCorrectContentType. O método verifica se o código de status da resposta foi bem-sucedido (200-299) e o cabeçalho Content-Type está text/html; charset=utf-8 para várias páginas do aplicativo.

CreateClient() cria uma instância de HttpClient que segue automaticamente redirecionamentos e lida com cookies.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

Por padrão, os cookies não essenciais não são preservados nas solicitações quando a política de consentimento do Regulamento Geral de Proteção de Dados está ativada. Para preservar cookies não essenciais, como os usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Cookies essenciais.

AngleSharp vs Application Parts para verificações antifalsificação

Este artigo usa o analisador AngleSharp para lidar com as verificações antifalsificação carregando páginas e analisando o HTML. Para testar os endereços de destino das visualizações do controlador e do Razor Pages a um nível mais baixo, sem se preocupar com a forma como são apresentados no navegador, considere a utilização de Application Parts. A abordagem Application Parts injeta um controlador ou Razor Page no aplicativo que pode ser usado para fazer solicitações JSON para obter os valores necessários. Para obter mais informações, consulte o blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts e de repositório GitHub associado pelo Martin Costello.

Personalizar WebApplicationFactory

A configuração do host da Web pode ser criada independentemente das classes de teste, herdando de WebApplicationFactory<TEntryPoint> para criar uma ou mais fábricas personalizadas:

  1. Herdar de WebApplicationFactory e substituir ConfigureWebHost. O IWebHostBuilder permite a configuração da coleção de serviços com IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    A propagação do banco de dados no aplicativo de exemplo é executada pelo método InitializeDbForTests. O método é descrito na seção Integration tests sample: Test app organization seção.

    O contexto da base de dados da SUT está registado em Program.cs. A execução do "callback" builder.ConfigureServices da aplicação de teste ocorre depois de o código Program.cs da aplicação ser executado. Para usar um banco de dados diferente para os testes em relação ao banco de dados da aplicação, o contexto do banco de dados da aplicação deve ser substituído em builder.ConfigureServices.

    O aplicativo de exemplo localiza o descritor de serviço para o contexto do banco de dados e usa o descritor para remover o registro de serviço. Em seguida, a fábrica adiciona um novo ApplicationDbContext que usa um banco de dados na memória para os testes.

    Para se conectar a um banco de dados diferente, altere o DbConnection. Para usar um banco de dados de teste do SQL Server:

  1. Use o CustomWebApplicationFactory personalizado em classes de teste. O exemplo a seguir usa a fábrica na classe IndexPageTests:

    public class IndexPageTests :
        IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program>
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<Program> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    

    O cliente do aplicativo de exemplo está configurado para impedir que o HttpClient siga redirecionamentos. Conforme explicado mais adiante na seção de autenticação simulada, isso permite que os testes verifiquem o resultado da primeira resposta do aplicativo. A primeira resposta é um redirecionamento em muitos desses testes com um cabeçalho Location.

  2. Um teste típico usa os métodos HttpClient e auxiliar para processar a solicitação e a resposta:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Qualquer solicitação POST ao SUT deve satisfazer a verificação antifalsificação realizada automaticamente pelo sistema de antifalsificação de proteção de dados do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:

  1. Faça um pedido para a página.
  2. Analise o cookie antifalsificação e solicite o token de validação da resposta.
  3. Faça a solicitação POST com o token antifalsificação cookie e solicite o token de validação válido.

Os métodos auxiliares de extensão SendAsync (Helpers/HttpClientExtensions.cs) e o método auxiliar GetDocumentAsync (Helpers/HtmlHelpers.cs) no aplicativo exemplo usam o analisador AngleSharp para lidar com a verificação antifalsificação com os seguintes métodos:

  • GetDocumentAsync: Recebe o HttpResponseMessage e devolve um IHtmlDocument. GetDocumentAsync usa uma fábrica que prepara uma resposta virtual com base no HttpResponseMessageoriginal. Para obter mais informações, consulte a documentação do AngleSharp .
  • SendAsync métodos de extensão para o HttpClient compõem um HttpRequestMessage e chamam SendAsync(HttpRequestMessage) para enviar solicitações ao SUT. As sobrecargas para SendAsync aceitam o formulário HTML (IHtmlFormElement) e o seguinte:
    • Botão Enviar do formulário (IHtmlElement)
    • Coleção de valores de formulário (IEnumerable<KeyValuePair<string, string>>)
    • Botão Enviar (IHtmlElement) e valores de formulário (IEnumerable<KeyValuePair<string, string>>)

AngleSharp é uma biblioteca de análise de terceiros usada para fins de demonstração neste artigo e na aplicação de exemplo. O AngleSharp não é suportado ou necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o Html Agility Pack (HAP). Outra abordagem é escrever código para lidar diretamente com o token de verificação de solicitações do sistema antifalsificação e o cookie de antifalsificação do sistema. Consulte AngleSharp vs Application Parts para verificações antifalsificação neste artigo para obter mais informações.

O EF-Core provedor de banco de dados na memória pode ser usado para testes limitados e básicos, no entanto, o provedor SQLite é a escolha recomendada para testes na memória.

Consulte Estender inicialização com filtros de inicialização, que mostra como configurar o middleware usando IStartupFilter, o que é útil quando um teste requer um serviço personalizado ou middleware.

Personalize o cliente com WithWebHostBuilder

Quando uma configuração adicional é necessária dentro de um método de teste, WithWebHostBuilder cria um novo WebApplicationFactory com um IWebHostBuilder que é ainda mais personalizado pela configuração.

O código de exemplo faz uma chamada ao WithWebHostBuilder para substituir serviços configurados por stubs de teste. Para obter mais informações e exemplos de uso, consulte de serviços simulados de injeção neste artigo.

O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot do aplicativo de exemplo demonstra o uso de WithWebHostBuilder. Este teste executa uma exclusão de registro no banco de dados acionando um envio de formulário no SUT.

Como outro teste na classe IndexPageTests executa uma operação que exclui todos os registros no banco de dados e pode ser executado antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot, o banco de dados é repropagado nesse método de teste para garantir que um registro esteja presente para o SUT excluir. A seleção do primeiro botão de exclusão do formulário messages no SUT é simulada na solicitação ao SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Opções do cliente

Consulte a página WebApplicationFactoryClientOptions para obter os padrões e as opções disponíveis ao criar instâncias HttpClient.

Crie a classe WebApplicationFactoryClientOptions e passe-a para o método CreateClient():

public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

NOTA: Para evitar avisos de redirecionamento HTTPS em logs ao usar o middleware de redirecionamento HTTPS, defina BaseAddress = new Uri("https://localhost")

Injetar serviços simulados

Os serviços podem ser substituídos em um teste com uma chamada para ConfigureTestServices no construtor de hosts. Para definir o escopo dos serviços substituídos para o teste em si, o método WithWebHostBuilder é usado para recuperar um construtor de host. Isso pode ser visto nos seguintes testes:

O SUT de exemplo inclui um serviço delimitado que retorna uma citação. A citação é incorporada num campo oculto na página Índice quando esta é solicitada.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Program.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

A seguinte marcação é gerada quando o aplicativo SUT é executado:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Para testar o serviço e a injeção de cotação em um teste de integração, um serviço simulado é injetado no SUT pelo teste. O serviço simulado substitui o QuoteService do aplicativo por um serviço fornecido pelo aplicativo de teste, chamado TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices é chamado e um serviço com um escopo é registado:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

A marcação produzida durante a execução do teste reflete o texto da citação fornecido por TestQuoteService, assim a afirmação confirma-se.

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Autenticação simulada

Os testes na classe AuthTests verificam um ponto de extremidade seguro:

  • Redireciona um usuário não autenticado para a página de login do aplicativo.
  • Retorna conteúdo para um usuário autenticado.

No SUT, a página /SecurePage usa uma convenção AuthorizePage para aplicar um AuthorizeFilter à página. Para mais informações, consulte as convenções de autorização de páginas Razor.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

No teste de Get_SecurePageRedirectsAnUnauthenticatedUser, um WebApplicationFactoryClientOptions é definido para não permitir redirecionamentos definindo AllowAutoRedirect como false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Ao não permitir que o cliente siga o redirecionamento, as seguintes verificações podem ser feitas:

  • O código de status retornado pelo SUT pode ser verificado em relação ao resultado HttpStatusCode.Redirect esperado, não o código de status final após o redirecionamento para a página de login, que seria HttpStatusCode.OK.
  • O valor do cabeçalho Location nos cabeçalhos de resposta é verificado para confirmar que começa com http://localhost/Identity/Account/Login, não com a resposta final da página de início de sessão, onde o cabeçalho Location não estaria presente.

O aplicativo de teste pode simular um AuthenticationHandler<TOptions> em ConfigureTestServices para testar aspetos de autenticação e autorização. Um cenário mínimo devolve um AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

O TestAuthHandler é chamado para autenticar um usuário quando o esquema de autenticação é definido como TestScheme onde AddAuthentication está registrado para ConfigureTestServices. É importante que o esquema de TestScheme corresponda ao esquema esperado pelo seu aplicativo. Caso contrário, a autenticação não funcionará.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Para obter mais informações sobre WebApplicationFactoryClientOptions, consulte a seção de opções de cliente.

Testes básicos para middleware de autenticação

Consulte este repositório GitHub para testes básicos de middleware de autenticação. Ele contém um de servidor de teste específico para o cenário de teste.

Definir o ambiente

Defina o ambiente na fábrica de aplicações personalizadas.

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Como a infraestrutura de teste infere o caminho raiz do conteúdo do aplicativo

O construtor WebApplicationFactory infere o aplicativo caminho de raiz de conteúdo procurando por um WebApplicationFactoryContentRootAttribute no assembly contendo os testes de integração com uma chave igual ao assembly TEntryPointSystem.Reflection.Assembly.FullName. Caso um atributo com a chave correta não seja encontrado, WebApplicationFactory volta à procura de um arquivo de solução (.sln) e acrescenta o nome do assembly TEntryPoint ao diretório da solução. O diretório raiz do aplicativo (o caminho da raiz do conteúdo) é usado para descobrir exibições e arquivos de conteúdo.

Desativar cópia de sombra

A cópia de sombra faz com que os testes sejam executados em um diretório diferente do diretório de saída. Se os testes dependerem do carregamento de ficheiros relativos ao Assembly.Location e surgirem problemas, talvez seja necessário desativar a cópia de sombra.

Para desabilitar a cópia de sombra ao usar xUnit, crie um arquivo de xunit.runner.json no diretório do projeto de teste, com a definição de configuração correta:

{
  "shadowCopy": false
}

Eliminação de objetos

Depois que os testes da implementação do IClassFixture são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory. Se os objetos instanciados pelo desenvolvedor precisarem ser eliminados, elimine-os na implementação IClassFixture. Para obter mais informações, consulte Implementing a Dispose method.

Exemplo de testes de integração

A aplicação de exemplo é composta por duas aplicações:.

Aplicação Diretório de projetos Descrição
Aplicativo de mensagem (o SUT) src/RazorPagesProject Permite que um usuário adicione, exclua um, exclua todos e analise mensagens.
Aplicativo de teste tests/RazorPagesProject.Tests Usado para testar a integração do SUT.

Os testes podem ser executados usando os recursos de teste internos de um IDE, como Visual Studio. Se estiver usando Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando no diretório tests/RazorPagesProject.Tests:

dotnet test

Organização do aplicativo de mensagem (SUT)

O SUT é um sistema de mensagens Razor Pages com as seguintes características:

  • A página Índice do aplicativo (Pages/Index.cshtml e Pages/Index.cshtml.cs) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (média de palavras por mensagem).
  • Uma mensagem é descrita pela classe Message (Data/Message.cs) com duas propriedades: Id (chave) e Text (mensagem). A propriedade Text é obrigatória e limitada a 200 caracteres.
  • As mensagens são armazenadas usando a base de dados na memória do Entity Framework †.
  • O aplicativo contém uma camada de acesso a dados (DAL) em sua classe de contexto de banco de dados, AppDbContext (Data/AppDbContext.cs).
  • Se o banco de dados estiver vazio na inicialização do aplicativo, o armazenamento de mensagens será inicializado com três mensagens.
  • O aplicativo inclui um /SecurePage que só pode ser acessado por um usuário autenticado.

†O artigo do EF, Test with InMemory, explica como usar um banco de dados na memória para testes com o MSTest. Este tópico usa o xUnit estrutura de teste. Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.

Embora o aplicativo não use o padrão de repositório e não seja um exemplo eficaz do padrão de Unidade de Trabalho (UoW), o Razor Pages oferece suporte a esses padrões de desenvolvimento. Para mais informações, consulte Projetar a camada de persistência da infraestrutura e testar a lógica do controlador (o exemplo implementa o padrão de repositório).

Testar a organização do aplicativo

O aplicativo de teste é um aplicativo de console dentro do diretório tests/RazorPagesProject.Tests.

Diretório de aplicativos de teste Descrição
AuthTests Contém métodos de teste para:
  • Aceder a uma página segura por um utilizador não autenticado.
  • Acesso a uma página segura por um utilizador autenticado com uma simulação AuthenticationHandler<TOptions>.
  • Obter um perfil de usuário do GitHub e verificar o login de usuário do perfil.
BasicTests Contém um método de teste para roteamento e tipo de conteúdo.
IntegrationTests Contém os testes de integração para a página Índice usando a classe WebApplicationFactory personalizada.
Helpers/Utilities
  • Utilities.cs contém o método InitializeDbForTests usado para semear o banco de dados com dados de teste.
  • HtmlHelpers.cs disponibiliza um método para devolver um AngleSharp IHtmlDocument a ser utilizado pelos métodos de teste.
  • HttpClientExtensions.cs fornecem sobrecargas para SendAsync enviar solicitações ao SUT.

A estrutura de teste é xUnit. Os testes de integração são realizados usando o Microsoft.AspNetCore.TestHost, que inclui o TestServer. Como o pacote Microsoft.AspNetCore.Mvc.Testing é usado para configurar o host de teste e o servidor de teste, os pacotes TestHost e TestServer não exigem referências diretas de pacote no arquivo de projeto do aplicativo de teste ou na configuração do desenvolvedor no aplicativo de teste.

Os testes de integração geralmente exigem um pequeno conjunto de dados no banco de dados antes da execução do teste. Por exemplo, um teste de exclusão chama para uma exclusão de registro de banco de dados, portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão seja bem-sucedida.

A aplicação de exemplo preenche a base de dados com três mensagens em Utilities.cs para utilização nos testes quando estes forem executados.

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

O contexto da base de dados da SUT está registado em Program.cs. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois de o código Program.cs do aplicativo é executado. Para usar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices. Para mais informações, consulte a secção Customize WebApplicationFactory.

Recursos adicionais

Este artigo pressupõe uma compreensão básica de testes de unidade. Se não estiver familiarizado com conceitos de teste, consulte o artigo Teste Unitário no .NET Core e .NET Standard e seu conteúdo vinculado.

Exibir ou baixar o código de exemplo (como baixar)

A aplicação de exemplo é uma aplicação Razor Páginas e pressupõe um entendimento básico de Razor Páginas. Se não estiver familiarizado com o Razor Pages, consulte os seguintes artigos:

Para testar SPAs, recomendamos uma ferramenta como Playwright for .NET, que pode automatizar um navegador.

Introdução aos testes de integração

Os testes de integração avaliam os componentes de um aplicativo em um nível mais amplo do que testes de unidade. Os testes de unidade são usados para testar componentes de software isolados, como métodos de classe individuais. Os testes de integração confirmam que dois ou mais componentes do aplicativo trabalham juntos para produzir um resultado esperado, possivelmente incluindo todos os componentes necessários para processar totalmente uma solicitação.

Esses testes mais amplos são usados para testar a infraestrutura e toda a estrutura do aplicativo, geralmente incluindo os seguintes componentes:

  • Base de dados
  • Sistema de ficheiros
  • Dispositivos de rede
  • Fluxo de solicitação-resposta

Os testes de unidade usam componentes fabricados, conhecidos como objetos fictícios ou objetos simulados, no lugar de componentes de infraestrutura.

Em contraste com os testes de unidade, os testes de integração:

  • Use os componentes reais que o aplicativo usa na produção.
  • Exigem mais código e processamento de dados.
  • Leve mais tempo para correr.

Portanto, limite o uso de testes de integração aos cenários de infraestrutura mais importantes. Se um comportamento puder ser testado usando um teste de unidade ou um teste de integração, escolha o teste de unidade.

Em discussões de testes de integração, o projeto testado é frequentemente chamado de System Under Test, ou "SUT" para abreviar. "SUT" é usado ao longo deste artigo para se referir ao aplicativo ASP.NET Core que está sendo testado.

Não escreva testes de integração para cada de permutação de dados e acesso a arquivos com bancos de dados e sistemas de arquivos. Independentemente de quantos lugares em um aplicativo interagem com bancos de dados e sistemas de arquivos, um conjunto focado de testes de integração de leitura, gravação, atualização e exclusão geralmente é capaz de testar adequadamente os componentes do banco de dados e do sistema de arquivos. Use testes de unidade para testes de rotina de lógica de método que interagem com esses componentes. Em testes de unidade, o uso de falsificações ou simulações de infraestrutura resulta em execução de teste mais rápida.

ASP.NET Testes de integração principais

Os testes de integração no ASP.NET Core requerem o seguinte:

  • Um projeto de teste é usado para conter e executar os testes. O projeto de teste tem uma referência ao SUT.
  • O projeto de teste cria um host de teste para o SUT e usa um cliente de servidor de teste para lidar com solicitações e respostas com o SUT.
  • Um corredor de teste é usado para executar os testes e relatar os resultados do teste.

Os testes de integração seguem uma sequência de eventos que incluem as etapas habituais de teste Preparar, Atuare Validar:

  1. O servidor web da SUT está configurado.
  2. Um cliente de servidor de teste é criado para enviar solicitações ao aplicativo.
  3. A etapa de teste Arrange é executada: A aplicação de teste prepara uma solicitação.
  4. A etapa de teste do Act é executada: o cliente envia a solicitação e recebe a resposta.
  5. A etapa de teste Assert é executada: A resposta real é validada como aprovada ou reprovada baseada em uma resposta esperada.
  6. O processo continua até que todos os testes sejam executados.
  7. Os resultados dos testes foram divulgados.

Normalmente, o host da Web de teste é configurado de forma diferente do host normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usados para os testes.

Os componentes de infraestrutura, como o host da Web de teste e o servidor de teste na memória (TestServer), são fornecidos ou gerenciados pelo pacote de Microsoft.AspNetCore.Mvc.Testing. O uso deste pacote simplifica a criação e execução de testes.

O pacote Microsoft.AspNetCore.Mvc.Testing lida com as seguintes tarefas:

  • Copia o arquivo de dependências (.deps) do SUT para o diretório bin do projeto de teste.
  • Define a raiz de conteúdo para a raiz do projeto SUT para que arquivos estáticos e páginas/visualizações sejam encontrados quando os testes forem executados.
  • Fornece a classe WebApplicationFactory para simplificar a inicialização do SUT com TestServer.

A documentação dos testes de unidade descreve como configurar um projeto de teste e uma ferramenta de execução de testes, juntamente com instruções detalhadas sobre como executar testes e recomendações sobre como nomear testes e classes de teste.

Separe os testes de unidade dos testes de integração em diferentes projetos. Separar os testes:

  • Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
  • Permite controlar qual conjunto de testes é executado.

Não há praticamente nenhuma diferença entre a configuração para testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença está na forma como os testes são nomeados. Em uma aplicação Razor Pages, os testes dos pontos de extremidade das páginas geralmente são nomeados conforme a classe do modelo de página (por exemplo, IndexPageTests para testar a integração de componentes para a página Índice). Em um aplicativo MVC, os testes geralmente são organizados por classes de controlador e nomeados após os controladores que eles testam (por exemplo, HomeControllerTests para testar a integração de componentes para o controlador Home).

Pré-requisitos do aplicativo de teste

O projeto de ensaio deve:

Esses pré-requisitos podem ser vistos no aplicativo de exemplo . Inspecione o arquivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. O aplicativo de exemplo usa a estrutura de teste xUnit e a biblioteca de analisador AngleSharp, portanto, o aplicativo de exemplo também faz referência a:

Em aplicativos que usam xunit.runner.visualstudio versão 2.4.2 ou posterior, o projeto de teste deve fazer referência ao pacote Microsoft.NET.Test.Sdk.

O Entity Framework Core também é usado nos testes. Consulte o arquivo de projeto no GitHub.

Ambiente SUT

Se a de ambiente do SUT não estiver definida, o padrão do ambiente será Desenvolvimento.

Testes básicos com o padrão WebApplicationFactory

Exponha a classe Program implicitamente definida para o projeto de teste seguindo um destes procedimentos:

  • Exponha os tipos internos da aplicação web para o projeto de teste. Isso pode ser feito no arquivo do projeto SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Torne a classe Program pública usando uma declaração parcial de classe:

    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    

    O aplicativo de exemplo usa a Program abordagem de classe parcial.

WebApplicationFactory<TEntryPoint> é usado para criar um TestServer para os testes de integração. TEntryPoint é a classe de ponto de entrada do SUT, geralmente Program.cs.

As classes de teste implementam uma interface de fixação de classe , representada por (IClassFixture), para indicar que a classe contém testes e para fornecer instâncias de objeto partilhadas para os testes dentro da classe.

A seguinte classe de teste, BasicTests, usa o WebApplicationFactory para inicializar o SUT e fornecer uma HttpClient para um método de teste, Get_EndpointsReturnSuccessAndCorrectContentType. O método verifica se o código de status da resposta é bem-sucedido (200-299) e se o cabeçalho Content-Type está text/html; charset=utf-8 em várias páginas da aplicação.

CreateClient() cria uma instância de HttpClient que segue automaticamente redirecionamentos e lida com cookies.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

Por padrão, os cookies não essenciais não são preservados nas solicitações quando a política de consentimento do Regulamento Geral de Proteção de Dados está ativada. Para preservar cookies não essenciais, como os usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Cookies essenciais.

AngleSharp vs Application Parts para verificações antifalsificação

Este artigo usa o analisador AngleSharp para lidar com as verificações antifalsificação carregando páginas e analisando o HTML. Para testar os pontos de extremidade das visualizações do controlador e do Razor Pages em um nível inferior, sem se preocupar com a forma como eles são renderizados no navegador, considere usar Application Parts. A abordagem Application Parts injeta um controlador ou Razor Page no aplicativo que pode ser usado para fazer solicitações JSON para obter os valores necessários. Para obter mais informações, consulte o blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts e e repositório GitHub associado por Martin Costello.

Personalizar WebApplicationFactory

A configuração do alojamento Web pode ser criada de forma independente das classes de teste, herdando de WebApplicationFactory<TEntryPoint>, para configurar uma ou mais fábricas personalizadas.

  1. Herdar de WebApplicationFactory e substituir ConfigureWebHost. O IWebHostBuilder permite configurar a coleção de serviços com o IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    A propagação do banco de dados no aplicativo de exemplo é executada pelo método InitializeDbForTests. O método é descrito no exemplo de testes de integração: organização do aplicativo de teste, seção .

    O contexto da base de dados da SUT está registado em Program.cs. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois de o código Program.cs do aplicativo é executado. Para utilizar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices.

    O aplicativo de exemplo localiza o descritor de serviço para o contexto do banco de dados e usa o descritor para remover o registro de serviço. Em seguida, a fábrica adiciona um novo ApplicationDbContext que usa um banco de dados na memória para os testes.

    Para se conectar a um banco de dados diferente, altere o DbConnection. Para usar um banco de dados de teste do SQL Server:

  1. Utilize o CustomWebApplicationFactory personalizado em classes de teste. O exemplo a seguir usa a fábrica na classe IndexPageTests:

    public class IndexPageTests :
        IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program>
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<Program> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    }
    

    O cliente do aplicativo de exemplo está configurado para impedir que o HttpClient siga redirecionamentos. Conforme explicado mais adiante na seção de autenticação simulada, isso permite que os testes verifiquem o resultado da primeira resposta do aplicativo. A primeira resposta é um redirecionamento em muitos desses testes com um cabeçalho Location.

  2. Um teste típico usa os métodos HttpClient e auxiliar para processar a solicitação e a resposta:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Qualquer solicitação POST ao SUT deve satisfazer a verificação antifalsificação feita automaticamente pelo sistema antifalsificação de proteção de dados do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:

  1. Faça um pedido para a página.
  2. Interprete o elemento antifalsificação cookie e solicite um token de validação da resposta.
  3. Faça a solicitação POST com o token antifalsificação cookie e o token de validação necessário incluído.

Os métodos de extensão auxiliar SendAsync (Helpers/HttpClientExtensions.cs) e o método auxiliar GetDocumentAsync (Helpers/HtmlHelpers.cs) na aplicação de exemplo usam o analisador AngleSharp para manipular a verificação antifalsificação com os seguintes métodos:

  • GetDocumentAsync: Recebe o HttpResponseMessage e devolve um IHtmlDocument. GetDocumentAsync usa uma fábrica que prepara uma resposta virtual com base no HttpResponseMessageoriginal. Para mais informações, consulte a documentação do AngleSharp .
  • SendAsync métodos de extensão para o HttpClient compõem um HttpRequestMessage e chamam SendAsync(HttpRequestMessage) para enviar solicitações ao SUT. Sobrecargas de SendAsync aceitam o formulário HTML (IHtmlFormElement) e o seguinte:
    • Botão Enviar do formulário (IHtmlElement)
    • Coleção de valores de formulário (IEnumerable<KeyValuePair<string, string>>)
    • Botão Enviar (IHtmlElement) e valores de formulário (IEnumerable<KeyValuePair<string, string>>)

AngleSharp é uma biblioteca de análise de terceiros usada para fins de demonstração neste artigo e no aplicativo de exemplo. O AngleSharp não é suportado ou necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o Html Agility Pack (HAP). Outra abordagem é escrever código para lidar com o token de verificação de solicitação do sistema antiforgery e o antiforgery cookie diretamente. Consulte AngleSharp vs Application Parts para verificações antifalsificação neste artigo para obter mais informações.

O EF-Core provedor de banco de dados na memória pode ser usado para testes limitados e básicos, no entanto, o provedor SQLite é a escolha recomendada para testes na memória.

Veja Estender a inicialização com filtros de inicialização que mostra como configurar o middleware usando IStartupFilter, o que é útil quando um teste requer um serviço personalizado ou middleware.

Personalize o cliente com WithWebHostBuilder

Quando uma configuração adicional é necessária dentro de um método de teste, WithWebHostBuilder cria um novo WebApplicationFactory com um IWebHostBuilder que é ainda mais personalizado pela configuração.

O código de exemplo chama WithWebHostBuilder para substituir serviços configurados por stubs de teste. Para obter mais informações e exemplos de uso, consulte de serviços simulados de injeção neste artigo.

O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot da aplicação de exemplo demonstra o uso de WithWebHostBuilder. Este teste executa uma exclusão de registro no banco de dados acionando um envio de formulário no SUT.

Como outro teste na classe IndexPageTests executa uma operação que exclui todos os registros no banco de dados e pode ser executado antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot, o banco de dados é repropagado nesse método de teste para garantir que um registro esteja presente para o SUT excluir. A seleção do primeiro botão de exclusão do formulário messages no SUT é simulada na solicitação ao SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Opções do cliente

Consulte a página WebApplicationFactoryClientOptions para obter os padrões e as opções disponíveis ao criar instâncias HttpClient.

Crie a classe WebApplicationFactoryClientOptions e passe-a para o método CreateClient():

public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }
}

NOTA: Para evitar avisos de redirecionamento HTTPS em logs ao usar o middleware de redirecionamento HTTPS, defina BaseAddress = new Uri("https://localhost")

Injetar serviços simulados

Os serviços podem ser substituídos em um teste com uma chamada para ConfigureTestServices no construtor de hosts. Para definir o escopo dos serviços substituídos para o teste em si, o método WithWebHostBuilder é usado para recuperar um construtor de host. Isso pode ser visto nos seguintes testes:

O SUT de exemplo inclui um serviço de alcance específico que retorna uma cotação. A citação é incorporada num campo oculto na página de Índice quando a página de Índice é solicitada.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Program.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

A seguinte marcação é gerada quando o aplicativo SUT é executado:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Para testar o serviço e a injeção de cotação em um teste de integração, um serviço simulado é injetado no SUT pelo teste. O serviço simulado substitui o QuoteService do aplicativo por um serviço fornecido pelo aplicativo de teste, chamado TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices é chamado e o serviço com escopo é registrado:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

A marcação produzida durante a execução do teste reflete o texto da citação fornecido por TestQuoteService, assim a asserção passa:

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Autenticação simulada

Os testes na classe AuthTests verificam se um ponto de extremidade seguro:

  • Redireciona um usuário não autenticado para a página de login do aplicativo.
  • Retorna conteúdo para um usuário autenticado.

No SUT, a página /SecurePage utiliza a convenção AuthorizePage para aplicar o AuthorizeFilter à página. Para obter mais informações, consulte as convenções de autorização das páginas Razor.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

No teste de Get_SecurePageRedirectsAnUnauthenticatedUser, um WebApplicationFactoryClientOptions é definido para não permitir redirecionamentos definindo AllowAutoRedirect como false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Ao não permitir que o cliente siga o redirecionamento, as seguintes verificações podem ser feitas:

  • O código de status retornado pelo SUT pode ser verificado em relação ao resultado HttpStatusCode.Redirect esperado, não o código de status final após o redirecionamento para a página de login, que seria HttpStatusCode.OK.
  • O valor do cabeçalho Location nos cabeçalhos de resposta é verificado para confirmar que começa com http://localhost/Identity/Account/Login, não com a resposta final da página de início de sessão, onde o cabeçalho Location não estaria presente.

O aplicativo de teste pode simular um AuthenticationHandler<TOptions> em ConfigureTestServices para testar aspetos de autenticação e autorização. Um cenário mínimo retorna um AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

O TestAuthHandler é chamado para autenticar um usuário quando o esquema de autenticação é definido como TestScheme onde AddAuthentication está registrado para ConfigureTestServices. É importante que o esquema de TestScheme corresponda ao esquema esperado pelo seu aplicativo. Caso contrário, a autenticação não funcionará.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Para obter mais informações sobre WebApplicationFactoryClientOptions, consulte a seção Opções do Cliente .

Testes básicos para middleware de autenticação

Consulte o repositório GitHub para obter testes básicos do middleware de autenticação. Ele contém um servidor de teste que é específico para o cenário de teste.

Definir o ambiente

Configure o ambiente na fábrica de aplicações personalizadas:

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Como a infraestrutura de teste infere o caminho raiz do conteúdo do aplicativo

O construtor WebApplicationFactory infere o aplicativo caminho de raiz de conteúdo procurando por um WebApplicationFactoryContentRootAttribute no assembly contendo os testes de integração com uma chave igual ao assembly TEntryPointSystem.Reflection.Assembly.FullName. Caso um atributo com a chave correta não seja encontrado, WebApplicationFactory recorre à busca de um ficheiro de solução (.sln) e adiciona o nome do assembly TEntryPoint ao diretório de solução. O diretório raiz do aplicativo (o caminho da raiz do conteúdo) é usado para descobrir exibições e arquivos de conteúdo.

Desativar cópia de sombra

A cópia de sombra faz com que os testes sejam executados em um diretório diferente do diretório de saída. Se os testes dependerem do carregamento de arquivos relativos ao Assembly.Location e você encontrar problemas, talvez seja necessário desabilitar a cópia de sombra.

Para desabilitar a cópia de sombra ao usar xUnit, crie um arquivo de xunit.runner.json no diretório do projeto de teste, com a definição de configuração correta:

{
  "shadowCopy": false
}

Eliminação de objetos

Depois que os testes da implementação do IClassFixture são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory. Se os objetos instanciados pelo desenvolvedor precisarem ser eliminados, elimine-os na implementação IClassFixture. Para obter mais informações, consulte Implementing a Dispose method.

Exemplo de testes de integração

O de aplicativo de exemplo é composto por dois aplicativos:

Aplicação Diretório de projetos Descrição
Aplicativo de mensagem (o SUT) src/RazorPagesProject Permite que um usuário adicione, exclua um, exclua todos e analise mensagens.
Aplicativo de teste tests/RazorPagesProject.Tests Usado para testar a integração do SUT.

Os testes podem ser executados usando os recursos de teste internos de um IDE, como Visual Studio. Se estiver usando Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando no diretório tests/RazorPagesProject.Tests:

dotnet test

Organização do aplicativo de mensagem (SUT)

O SUT é um sistema de mensagens Razor Pages com as seguintes características:

  • A página Índice do aplicativo (Pages/Index.cshtml e Pages/Index.cshtml.cs) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (média de palavras por mensagem).
  • Uma mensagem é descrita pela classe Message (Data/Message.cs) com duas propriedades: Id (chave) e Text (mensagem). A propriedade Text é obrigatória e limitada a 200 caracteres.
  • As mensagens são armazenadas utilizando o serviço de banco de dados na memória do Entity Framework †.
  • O aplicativo contém uma camada de acesso a dados (DAL) em sua classe de contexto de banco de dados, AppDbContext (Data/AppDbContext.cs).
  • Se o banco de dados estiver vazio na inicialização do aplicativo, o armazenamento de mensagens será inicializado com três mensagens.
  • O aplicativo inclui um /SecurePage que só pode ser acessado por um usuário autenticado.

†O artigo do EF, Test with InMemory, explica como usar um banco de dados na memória para testes com o MSTest. Este tópico utiliza a estrutura de teste xUnit. Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.

Embora o aplicativo não use o padrão de repositório e não seja um exemplo eficaz do padrão de Unidade de Trabalho (UoW), o Razor Pages oferece suporte a esses padrões de desenvolvimento. Para mais informações, consulte Conceber a camada de persistência de infraestrutura e a lógica do controlador de teste (o exemplo implementa o padrão de repositório).

Testar a organização do aplicativo

O aplicativo de teste é um aplicativo de console dentro do diretório tests/RazorPagesProject.Tests.

Diretório de aplicativos de teste Descrição
AuthTests Contém métodos de teste para:
  • Aceder a uma página segura por um utilizador não autenticado.
  • Aceder a uma página segura por um utilizador autenticado com uma simulação AuthenticationHandler<TOptions>.
  • Obter um perfil de usuário do GitHub e verificar o login de usuário do perfil.
BasicTests Contém um método de teste para roteamento e tipo de conteúdo.
IntegrationTests Contém os testes de integração para a página Índice usando a classe WebApplicationFactory personalizada.
Helpers/Utilities
  • Utilities.cs contém o método InitializeDbForTests usado para semear o banco de dados com dados de teste.
  • HtmlHelpers.cs fornece um método para devolver um AngleSharp IHtmlDocument para ser utilizado pelos métodos de teste.
  • HttpClientExtensions.cs fornece sobrecargas para SendAsync enviar pedidos ao SUT.

A estrutura de teste é xUnit. Os testes de integração são realizados usando o Microsoft.AspNetCore.TestHost, que inclui o TestServer. Como o pacote Microsoft.AspNetCore.Mvc.Testing é usado para configurar o host de teste e o servidor de teste, os pacotes TestHost e TestServer não exigem referências diretas de pacote no arquivo de projeto do aplicativo de teste ou na configuração do desenvolvedor no aplicativo de teste.

Os testes de integração geralmente exigem um pequeno conjunto de dados no banco de dados antes da execução do teste. Por exemplo, um teste de exclusão chama para uma exclusão de registro de banco de dados, portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão seja bem-sucedida.

O aplicativo de exemplo semeia o banco de dados com três mensagens em Utilities.cs que os testes podem usar quando são executados:

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

O contexto da base de dados da SUT está registado em Program.cs. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois de o código Program.cs do aplicativo é executado. Para usar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices. Para obter mais informações, consulte a seção Personalizar WebApplicationFactory.

Recursos adicionais