Testes de integração no ASP.NET Core

Por Jos van der Til, Martin Costello e 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. ASP.NET Core dá suporte a testes de integração usando uma estrutura de teste de unidade com um host Web de teste e um servidor de teste na memória.

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

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

O aplicativo de exemplo é um Razor aplicativo Pages e pressupõe uma compreensão básica do Razor Pages. Se você não estiver familiarizado com o Razor Pages, consulte os seguintes artigos:

Para testar SPAs, recomendamos uma ferramenta como o Playwright para .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 os 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 de 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 do aplicativo e toda a estrutura, geralmente incluindo os seguintes componentes:

  • Banco de dados
  • Sistema de arquivos
  • Dispositivos de rede
  • Pipeline de solicitação-resposta

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

Ao contrário dos testes de unidade, os testes de integração:

  • Usam os componentes reais que o aplicativo usa em produção.
  • Exigem mais código e processamento de dados.
  • Demoraram mais para serem executados.

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 sobre 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 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 da lógica do método que interage com esses componentes. Em testes de unidade, o uso de infraestruturas falsas ou fictícias resulta em uma execução de teste mais rápida.

Testes de integração de ASP.NET Core

Os testes de integração no ASP.NET Core exigem 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 Web de teste para o SUT e usa um cliente do servidor de teste para lidar com solicitações e respostas com o SUT.
  • Um executor de teste é usado para executar os testes e relatar os resultados.

Os testes de integração seguem uma sequência de eventos que incluem as etapas de teste Organizar, Atuar e Afirmar:

  1. O host da Web do SUT está configurado.
  2. Um cliente do 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 Atuar é executada: o cliente envia a solicitação e recebe a resposta.
  5. A etapa de teste Afirmar é executada: a resposta real é validada como uma aprovada ou reprovada com base em uma resposta esperada.
  6. O processo continua até que todos os testes sejam executados.
  7. Os resultados do teste são reportados.

Normalmente, o host da Web de teste é configurado de forma diferente do host da Web normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usadas 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 desse pacote simplifica a criação e a execução do teste.

O pacote Microsoft.AspNetCore.Mvc.Testing manipula 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 do SUT para que arquivos estáticos e páginas/exibições sejam encontradas quando os testes forem executados.
  • Fornece a classe WebApplicationFactory para simplificar a inicialização do aplicativo testado com TestServer.

A documentação de testes de unidade descreve como configurar um projeto de teste e um executor de teste, 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 projetos diferentes. Separando os testes:

  • Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
  • Permite o controle sobre quais conjuntos de testes são executados.

Praticamente não há diferença entre a configuração de testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença é como os testes são nomeados. Em um aplicativo Razor Pages, os testes de pontos de extremidade de página geralmente recebem o mesmo nome da classe de 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 recebem o mesmo nome dos controladores que testam (por exemplo, HomeControllerTests para testar a integração de componentes para o controlador Home).

Pré-requisitos do aplicativo de teste

O projeto de teste deve:

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

Em aplicativos que usam xunit.runner.visualstudio na versão 2.4.2 ou posterior, o projeto de teste deve referenciar o 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 do SUT não estiver definido, o ambiente usará como padrão o Desenvolvimento.

Testes básicos com o WebApplicationFactory padrão

Exponha a classe definida Program implicitamente ao projeto de teste realizando um dos seguintes procedimentos:

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

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

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

    O aplicativo 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 acessório de classe (IClassFixture) para indicar que a classe contém testes e fornece instâncias de objeto compartilhadas entre os testes na classe .

A classe de teste a seguir, BasicTests, usa o WebApplicationFactory para iniciar o SUT e fornecer um HttpClient para 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 é text/html; charset=utf-8 em várias páginas do aplicativo.

CreateClient() cria uma instância do HttpClient que segue automaticamente redirecionamentos e manipula 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 entre solicitações quando a política de consentimento do Regulamento Geral de Proteção de Dados está habilitada. Para preservar cookies não essenciais, como aqueles usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Essential cookies.

Comparação entre AngleSharp e Application Parts a respeito de verificações de proteção contra falsificações

Este artigo usa o analisador AngleSharp para manipular as verificações de proteção contra falsificações carregando páginas e analisando o HTML. Para testar os pontos de extremidade do controlador e as visualizações do Razor Pages em um nível inferior, sem se preocupar com a forma como eles são renderizados no navegador, considere o uso de Application Parts. A abordagem Partes do Aplicativo injeta um controlador ou Página Razor 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 Recursos de Teste de Integração do ASP.NET Core Protegido contra Falsificações Usando Partes do Aplicativo e o repositório GitHub associado por Martin Costello.

Personalizar WebApplicationFactory

A configuração de 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 do 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 de banco de dados no aplicativo de exemplo é executada pelo método InitializeDbForTests. O método é descrito na seção Exemplo de testes de integração: testar a organização do aplicativo .

    O contexto de banco de dados do SUT é registrado em Program.cs. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois que o código Program.cs do aplicativo é executado. Para usar um banco de dados diferente para os testes além 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 do 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 evitar que HttpClient siga redirecionamentos. Conforme explicado posteriormente na seção Autenticação fictícia, 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 HttpClient e métodos auxiliares 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 para o SUT deve atender a verificação contra falsificações feita automaticamente pelo sistema de proteção de dados contra falsificações do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:

  1. Fazer uma solicitação para a página.
  2. Analisar o cookie de validação de solicitação e antifalsificação da resposta.
  3. Faça a solicitação POST com o token de validação de solicitação e antifalsificação cookie em vigor.

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

  • GetDocumentAsync: recebe o HttpResponseMessage e retorna um IHtmlDocument. GetDocumentAsync usa uma fábrica que prepara uma resposta virtual com base no HttpResponseMessage original. Para obter mais informações, consulte a documentação do AngleSharp.
  • SendAsync métodos de extensão para a composição HttpClient de um HttpRequestMessage e fazer a chamada de SendAsync(HttpRequestMessage) e para enviar solicitações para o SUT. As sobrecargas para SendAsync aceitam o formulário HTML (IHtmlFormElement) e os seguintes itens:
    • Botão Enviar do formulário (IHtmlElement)
    • Coleção de valores do formulário (IEnumerable<KeyValuePair<string, string>>)
    • Botão Enviar (IHtmlElement) e valores do formulário (IEnumerable<KeyValuePair<string, string>>)

O 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 tem suporte nem é necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o HAP (Pacote de Agilidade Html). Outra abordagem é escrever código para lidar diretamente com o token de verificação de solicitação do sistema antifalsificação e com o cookie antifalsificação diretamente. Consulte AngleSharp vs Application Parts para verificações antifalsificação neste artigo para obter mais informações.

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

Consulte 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 ou middleware personalizado.

Personalizar o cliente com WithWebHostBuilder

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

O código de exemplo chama WithWebHostBuilder para substituir serviços configurados com stubs de teste. Para obter mais informações e exemplos de uso, consulte Injetar serviços fictícios neste artigo.

O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot do aplicativo de exemplo demonstra o uso de WithWebHostBuilder. Esse teste executa uma exclusão de registro no banco de dados disparando 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 executada antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot, o banco de dados é propagado novamente neste método de teste para garantir que o registro esteja presente para o SUT excluir. A seleção do primeiro botão excluir do formulário messages do SUT é simulada na solicitação para o 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 ver as definições padrão 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 fictícios

Os serviços podem ser substituídos em um teste com uma chamada para xref:Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.ConfigureTestServices%2A no construtor de host. Para definir o escopo dos serviços substituídos para o próprio teste, 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 com escopo que retorna uma citação. A citação é integrada em um 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 marcação a seguir é 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 a injeção de serviço e de citação em um teste de integração, um serviço fictício é injetado no SUT pelo teste. O serviço fictício 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 entre aspas fornecido por TestQuoteService, portanto, 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 fictícia

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

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

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

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

No teste Get_SecurePageRedirectsAnUnauthenticatedUser, um WebApplicationFactoryClientOptions é definido para revogar a permissão de 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 esperado HttpStatusCode.Redirect, não o código de status final após o redirecionamento para a página de entrada, 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 a resposta final da página de entrada, em que o cabeçalho Location não estaria presente.

O aplicativo de teste pode simular um AuthenticationHandler<TOptions> no ConfigureTestServices para testar aspectos 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 TestScheme corresponda ao esquema que seu aplicativo espera. 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, confira a seção Opções do cliente.

Testes básicos para middlewares de autenticação

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

Definir o ambiente

Defina o ambiente na fábrica de aplicativos personalizada:

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 de conteúdo do aplicativo

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

Desabilitar a 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 seus testes dependerem do carregamento de arquivos relativos a 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 xunit.runner.json no diretório do projeto de teste, com a configuração correta:

{
  "shadowCopy": false
}

Descarte de objetos

Depois que os testes da implementação de IClassFixture são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory. Se os objetos instanciados pelo desenvolvedor exigirem descarte, descarte-os na IClassFixture implementação. Para saber mais, confira Implementação de um método Dispose.

Exemplo de testes de integração

O aplicativo de exemplo é composto por dois aplicativos:

Aplicativo Diretório do projeto Descrição
Aplicativo de mensagens (o SUT) src/RazorPagesProject Permite que o usuário adicione, exclua uma ou todas as mensagens e as analise.
Aplicativo de teste tests/RazorPagesProject.Tests Usado para fazer o teste de integração do SUT.

Os testes podem ser executados usando os recursos de teste nativos do IDE, como o Visual Studio. Se estiver usando o Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando na pasta tests/RazorPagesProject.Tests:

dotnet test

Organização do aplicativo de mensagens (SUT)

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

  • A página de Í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 (número médio de palavras por mensagem).
  • Uma mensagem é descrita pela classe Message (Data/Message.cs) com duas propriedades: Id (chave) e Text (mensagem). A propriedade Text é necessá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 DAL (camada de acesso a dados) 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 repositório 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 do EF, Teste com 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 do repositório e não seja um exemplo eficaz do padrão UoW (Unidade de Trabalho), o Razor Pages dá suporte a esses padrões de desenvolvimento. Para obter mais informações, confira Criando a camada de persistência da infraestrutura e Lógica do controlador de teste (o exemplo implementa o padrão do repositório).

Organização do aplicativo de teste

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

Diretório do aplicativo de teste Descrição
AuthTests Contém métodos de teste para:
  • Acessar uma página segura por um usuário não autenticado.
  • Acessando uma página segura por um usuário autenticado com um AuthenticationHandler<TOptions> fictício.
  • Obter um perfil de usuário do GitHub e verificar o logon do 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 de Índice usando a classe personalizada WebApplicationFactory .
Helpers/Utilities
  • Utilities.cs contém o método InitializeDbForTests usado para propagar o banco de dados com dados de teste.
  • HtmlHelpers.cs fornece um método para retornar um AngleSharp IHtmlDocument para uso pelos métodos de teste.
  • HttpClientExtensions.cs forneça sobrecargas para que SendAsync envie solicitações para o 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 do 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 exige a exclusão de um registro do banco de dados; portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão tenha êxito.

O aplicativo de exemplo propaga 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 de banco de dados do SUT é registrado em Program.cs. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois que 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

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

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

O aplicativo de exemplo é um Razor aplicativo Pages e pressupõe uma compreensão básica do 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 para .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 os 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 de 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 do aplicativo e toda a estrutura, geralmente incluindo os seguintes componentes:

  • Banco de dados
  • Sistema de arquivos
  • Dispositivos de rede
  • Pipeline de solicitação-resposta

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

Ao contrário dos testes de unidade, os testes de integração:

  • Usam os componentes reais que o aplicativo usa em produção.
  • Exigem mais código e processamento de dados.
  • Demoraram mais para serem executados.

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 sobre 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 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 da lógica do método que interage com esses componentes. Em testes de unidade, o uso de infraestruturas falsas ou fictícias resulta em uma execução de teste mais rápida.

Testes de integração de ASP.NET Core

Os testes de integração no ASP.NET Core exigem 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 Web de teste para o SUT e usa um cliente do servidor de teste para lidar com solicitações e respostas com o SUT.
  • Um executor de teste é usado para executar os testes e relatar os resultados.

Os testes de integração seguem uma sequência de eventos que incluem as etapas de teste Organizar, Atuar e Afirmar:

  1. O host da Web do SUT está configurado.
  2. Um cliente do 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 Atuar é executada: o cliente envia a solicitação e recebe a resposta.
  5. A etapa de teste Afirmar é executada: a resposta real é validada como uma aprovada ou reprovada com base em uma resposta esperada.
  6. O processo continua até que todos os testes sejam executados.
  7. Os resultados do teste são reportados.

Normalmente, o host da Web de teste é configurado de forma diferente do host da Web normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usadas 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 desse pacote simplifica a criação e a execução do teste.

O pacote Microsoft.AspNetCore.Mvc.Testing manipula 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 do SUT para que arquivos estáticos e páginas/exibições sejam encontradas quando os testes forem executados.
  • Fornece a classe WebApplicationFactory para simplificar a inicialização do aplicativo testado com TestServer.

A documentação de testes de unidade descreve como configurar um projeto de teste e um executor de teste, 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 projetos diferentes. Separando os testes:

  • Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
  • Permite o controle sobre quais conjuntos de testes são executados.

Praticamente não há diferença entre a configuração de testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença é como os testes são nomeados. Em um aplicativo Razor Pages, os testes de pontos de extremidade de página geralmente recebem o mesmo nome da classe de 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 recebem o mesmo nome dos controladores que testam (por exemplo, HomeControllerTests para testar a integração de componentes para o controlador Home).

Pré-requisitos do aplicativo de teste

O projeto de teste deve:

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

Em aplicativos que usam xunit.runner.visualstudio na versão 2.4.2 ou posterior, o projeto de teste deve referenciar o pacote Microsoft.NET.Test.Sdk.

O Entity Framework Core também é usado nos testes. O aplicativo faz referência:

Ambiente do SUT

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

Testes básicos com o WebApplicationFactory padrão

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 acessório de classe (IClassFixture) para indicar que a classe contém testes e fornece instâncias de objeto compartilhadas entre os testes na classe .

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

CreateClient() cria uma instância do HttpClient que segue automaticamente redirecionamentos e manipula 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 cookie não essenciais não são preservados entre solicitações quando a Política de consentimento do GDPR está habilitada. Para preservar cookies não essenciais, como aqueles usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Essential cookies.

Personalizar WebApplicationFactory

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

  1. Herdar do 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 de banco de dados no aplicativo de exemplo é executada pelo método InitializeDbForTests. O método é descrito na seção Exemplo de testes de integração: testar a organização do aplicativo .

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

    Nos SUTs que ainda usam o Host Da Web, o retorno de chamada builder.ConfigureServices do aplicativo de teste é executado antes do código Startup.ConfigureServices do SUT. O retorno de chamada 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 do 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 evitar que HttpClient siga redirecionamentos. Conforme explicado posteriormente na seção Autenticação fictícia, 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 HttpClient e métodos auxiliares 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 para o SUT deve atender a verificação contra falsificações feita automaticamente pelo sistema de proteção de dados contra falsificações do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:

  1. Fazer uma solicitação para a página.
  2. Analisar o cookie de validação de solicitação e antifalsificação da resposta.
  3. Faça a solicitação POST com o token de validação de solicitação e antifalsificação cookie em vigor.

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

  • GetDocumentAsync: recebe o HttpResponseMessage e retorna um IHtmlDocument. GetDocumentAsync usa uma fábrica que prepara uma resposta virtual com base no HttpResponseMessage original. Para obter mais informações, consulte a documentação do AngleSharp.
  • SendAsync métodos de extensão para a composição HttpClient de um HttpRequestMessage e fazer a chamada de SendAsync(HttpRequestMessage) e para enviar solicitações para o SUT. As sobrecargas para SendAsync aceitam o formulário HTML (IHtmlFormElement) e os seguintes itens:
    • Botão Enviar do formulário (IHtmlElement)
    • Coleção de valores do formulário (IEnumerable<KeyValuePair<string, string>>)
    • Botão Enviar (IHtmlElement) e valores do formulário (IEnumerable<KeyValuePair<string, string>>)

Observação

O 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 tem suporte nem é necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o HAP (Pacote de Agilidade Html). Outra abordagem é escrever código para lidar diretamente com o token de verificação de solicitação do sistema antifalsificação e com o cookie antifalsificação diretamente.

Observação

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

Personalizar o cliente com WithWebHostBuilder

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

O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot do aplicativo de exemplo demonstra o uso de WithWebHostBuilder. Esse teste executa uma exclusão de registro no banco de dados disparando 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 executada antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot, o banco de dados é propagado novamente neste método de teste para garantir que o registro esteja presente para o SUT excluir. A seleção do primeiro botão excluir do formulário messages do SUT é simulada na solicitação para o 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 de HttpClient.

Opção Descrição Padrão
AllowAutoRedirect Obtém ou define se as instâncias de HttpClient devem ou não seguir automaticamente as respostas de redirecionamento. true
BaseAddress Obtém ou define o endereço básico das instâncias de HttpClient. http://localhost
HandleCookies Obtém ou define se as instâncias de HttpClient devem manipular cookies. true
MaxAutomaticRedirections Obtém ou define o número máximo de respostas de redirecionamento que as instâncias de HttpClient 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 fictícios

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

O SUT de exemplo inclui um serviço com escopo que retorna uma citação. A citação é integrada em um 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.");
    }
}

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 marcação a seguir é 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 a injeção de serviço e de citação em um teste de integração, um serviço fictício é injetado no SUT pelo teste. O serviço fictício 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 entre aspas fornecido por TestQuoteService, portanto, 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 fictícia

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

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

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

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

No teste Get_SecurePageRedirectsAnUnauthenticatedUser, um WebApplicationFactoryClientOptions é definido para revogar a permissão de 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 esperado HttpStatusCode.Redirect, não ao 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 a resposta final da página de login, em que o cabeçalho Location não estaria presente.

O aplicativo de teste pode simular um AuthenticationHandler<TOptions> no ConfigureTestServices para testar aspectos 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 Test corresponda ao esquema que seu aplicativo espera. 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, confira a seção Opções do cliente.

Definir o ambiente

Por padrão, o ambiente de host e aplicativo do SUT é configurado para usar o ambiente de Desenvolvimento. Para substituir o ambiente do SUT ao usar IHostBuilder:

  • Defina a variável de ambiente ASPNETCORE_ENVIRONMENT (por exemplo, Staging, Production ou 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 de conteúdo do aplicativo

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

Desabilitar a 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 seus testes dependerem do carregamento de arquivos relativos a 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 xunit.runner.json no diretório do projeto de teste, com a configuração correta:

{
  "shadowCopy": false
}

Descarte de objetos

Depois que os testes da implementação de IClassFixture são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory. Se os objetos instanciados pelo desenvolvedor exigirem descarte, descarte-os na IClassFixture implementação. Para saber mais, confira Implementação de um método Dispose.

Exemplo de testes de integração

O aplicativo de exemplo é composto por dois aplicativos:

Aplicativo Diretório do projeto Descrição
Aplicativo de mensagens (o SUT) src/RazorPagesProject Permite que o usuário adicione, exclua uma ou todas as mensagens e as analise.
Aplicativo de teste tests/RazorPagesProject.Tests Usado para fazer o teste de integração do SUT.

Os testes podem ser executados usando os recursos de teste nativos do IDE, como o Visual Studio. Se estiver usando o Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando na pasta tests/RazorPagesProject.Tests:

dotnet test

Organização do aplicativo de mensagens (SUT)

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

  • A página de Í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 (número médio de palavras por mensagem).
  • Uma mensagem é descrita pela classe Message (Data/Message.cs) com duas propriedades: Id (chave) e Text (mensagem). A propriedade Text é necessá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 DAL (camada de acesso a dados) 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 repositório 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 do EF, Teste com 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 do repositório e não seja um exemplo eficaz do padrão UoW (Unidade de Trabalho), o Razor Pages dá suporte a esses padrões de desenvolvimento. Para obter mais informações, confira Criando a camada de persistência da infraestrutura e Lógica do controlador de teste (o exemplo implementa o padrão do repositório).

Organização do aplicativo de teste

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

Diretório do aplicativo de teste Descrição
AuthTests Contém métodos de teste para:
  • Acessar uma página segura por um usuário não autenticado.
  • Acessando uma página segura por um usuário autenticado com um AuthenticationHandler<TOptions> fictício.
  • Obter um perfil de usuário do GitHub e verificar o logon do 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 de Índice usando a classe personalizada WebApplicationFactory .
Helpers/Utilities
  • Utilities.cs contém o método InitializeDbForTests usado para propagar o banco de dados com dados de teste.
  • HtmlHelpers.cs fornece um método para retornar um AngleSharp IHtmlDocument para uso pelos métodos de teste.
  • HttpClientExtensions.cs forneça sobrecargas para que SendAsync envie solicitações para o 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 do 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 exige a exclusão de um registro do banco de dados; portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão tenha êxito.

O aplicativo de exemplo propaga 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 de banco de dados do SUT é registrado em seu método Startup.ConfigureServices. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois que o código Startup.ConfigureServices 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 .

Nos SUTs que ainda usam o Host Da Web, o retorno de chamada builder.ConfigureServices do aplicativo de teste é executado antes do 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 sobre testes de unidade. Se não estiver familiarizado com os conceitos de teste, consulte o artigo Teste de Unidade no .NET Core e no .NET Standard e seu conteúdo vinculado.

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

O aplicativo de exemplo é um Razor aplicativo Pages e pressupõe uma compreensão básica do Razor Pages. Se você não estiver familiarizado com o Razor Pages, consulte os seguintes artigos:

Para testar SPAs, recomendamos uma ferramenta como o Playwright para .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 os 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 de 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 do aplicativo e toda a estrutura, geralmente incluindo os seguintes componentes:

  • Banco de dados
  • Sistema de arquivos
  • Dispositivos de rede
  • Pipeline de solicitação-resposta

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

Ao contrário dos testes de unidade, os testes de integração:

  • Usam os componentes reais que o aplicativo usa em produção.
  • Exigem mais código e processamento de dados.
  • Demoraram mais para serem executados.

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 sobre 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 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 da lógica do método que interage com esses componentes. Em testes de unidade, o uso de infraestruturas falsas ou fictícias resulta em uma execução de teste mais rápida.

Testes de integração de ASP.NET Core

Os testes de integração no ASP.NET Core exigem 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 Web de teste para o SUT e usa um cliente do servidor de teste para lidar com solicitações e respostas com o SUT.
  • Um executor de teste é usado para executar os testes e relatar os resultados.

Os testes de integração seguem uma sequência de eventos que incluem as etapas de teste Organizar, Atuar e Afirmar:

  1. O host da Web do SUT está configurado.
  2. Um cliente do 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 Atuar é executada: o cliente envia a solicitação e recebe a resposta.
  5. A etapa de teste Afirmar é executada: a resposta real é validada como uma aprovada ou reprovada com base em uma resposta esperada.
  6. O processo continua até que todos os testes sejam executados.
  7. Os resultados do teste são reportados.

Normalmente, o host da Web de teste é configurado de forma diferente do host da Web normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usadas 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 desse pacote simplifica a criação e a execução do teste.

O pacote Microsoft.AspNetCore.Mvc.Testing manipula 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 do SUT para que arquivos estáticos e páginas/exibições sejam encontradas quando os testes forem executados.
  • Fornece a classe WebApplicationFactory para simplificar a inicialização do aplicativo testado com TestServer.

A documentação de testes de unidade descreve como configurar um projeto de teste e um executor de teste, 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 projetos diferentes. Separando os testes:

  • Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
  • Permite o controle sobre quais conjuntos de testes são executados.

Praticamente não há diferença entre a configuração de testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença é como os testes são nomeados. Em um aplicativo Razor Pages, os testes de pontos de extremidade de página geralmente recebem o mesmo nome da classe de 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 recebem o mesmo nome dos controladores que testam (por exemplo, HomeControllerTests para testar a integração de componentes para o controlador Home).

Pré-requisitos do aplicativo de teste

O projeto de teste deve:

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

Em aplicativos que usam xunit.runner.visualstudio na versão 2.4.2 ou posterior, o projeto de teste deve referenciar o 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 do SUT não estiver definido, o ambiente usará como padrão o Desenvolvimento.

Testes básicos com o WebApplicationFactory padrão

Exponha a classe definida Program implicitamente ao projeto de teste realizando um dos seguintes procedimentos:

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

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

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

    O aplicativo 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 acessório de classe (IClassFixture) para indicar que a classe contém testes e fornece instâncias de objeto compartilhadas entre os testes na classe .

A classe de teste a seguir, BasicTests, usa o WebApplicationFactory para iniciar o SUT e fornecer um HttpClient para 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 é text/html; charset=utf-8 em várias páginas do aplicativo.

CreateClient() cria uma instância do HttpClient que segue automaticamente redirecionamentos e manipula 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 entre solicitações quando a política de consentimento do Regulamento Geral de Proteção de Dados está habilitada. Para preservar cookies não essenciais, como aqueles usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Essential cookies.

Comparação entre AngleSharp e Application Parts a respeito de verificações de proteção contra falsificações

Este artigo usa o analisador AngleSharp para manipular as verificações de proteção contra falsificações carregando páginas e analisando o HTML. Para testar os pontos de extremidade do controlador e as visualizações do Razor Pages em um nível inferior, sem se preocupar com a forma como eles são renderizados no navegador, considere o uso de Application Parts. A abordagem Partes do Aplicativo injeta um controlador ou Página Razor 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 Recursos de Teste de Integração do ASP.NET Core Protegido contra Falsificações Usando Partes do Aplicativo e o repositório GitHub associado por Martin Costello.

Personalizar WebApplicationFactory

A configuração de 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 do 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 de banco de dados no aplicativo de exemplo é executada pelo método InitializeDbForTests. O método é descrito na seção Exemplo de testes de integração: testar a organização do aplicativo .

    O contexto de banco de dados do SUT é registrado em Program.cs. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois que o código Program.cs do aplicativo é executado. Para usar um banco de dados diferente para os testes além 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 do 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 evitar que HttpClient siga redirecionamentos. Conforme explicado posteriormente na seção Autenticação fictícia, 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 HttpClient e métodos auxiliares 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 para o SUT deve atender a verificação contra falsificações feita automaticamente pelo sistema de proteção de dados contra falsificações do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:

  1. Fazer uma solicitação para a página.
  2. Analisar o cookie de validação de solicitação e antifalsificação da resposta.
  3. Faça a solicitação POST com o token de validação de solicitação e antifalsificação cookie em vigor.

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

  • GetDocumentAsync: recebe o HttpResponseMessage e retorna um IHtmlDocument. GetDocumentAsync usa uma fábrica que prepara uma resposta virtual com base no HttpResponseMessage original. Para obter mais informações, consulte a documentação do AngleSharp.
  • SendAsync métodos de extensão para a composição HttpClient de um HttpRequestMessage e fazer a chamada de SendAsync(HttpRequestMessage) e para enviar solicitações para o SUT. As sobrecargas para SendAsync aceitam o formulário HTML (IHtmlFormElement) e os seguintes itens:
    • Botão Enviar do formulário (IHtmlElement)
    • Coleção de valores do formulário (IEnumerable<KeyValuePair<string, string>>)
    • Botão Enviar (IHtmlElement) e valores do formulário (IEnumerable<KeyValuePair<string, string>>)

O 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 tem suporte nem é necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o HAP (Pacote de Agilidade Html). Outra abordagem é escrever código para lidar diretamente com o token de verificação de solicitação do sistema antifalsificação e com o cookie antifalsificação diretamente. Consulte AngleSharp vs Application Parts para verificações antifalsificação neste artigo para obter mais informações.

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

Consulte 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 ou middleware personalizado.

Personalizar o cliente com WithWebHostBuilder

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

O código de exemplo chama WithWebHostBuilder para substituir serviços configurados com stubs de teste. Para obter mais informações e exemplos de uso, consulte Injetar serviços fictícios neste artigo.

O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot do aplicativo de exemplo demonstra o uso de WithWebHostBuilder. Esse teste executa uma exclusão de registro no banco de dados disparando 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 executada antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot, o banco de dados é propagado novamente neste método de teste para garantir que o registro esteja presente para o SUT excluir. A seleção do primeiro botão excluir do formulário messages do SUT é simulada na solicitação para o 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 ver as definições padrão 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 fictícios

Os serviços podem ser substituídos em um teste com uma chamada para xref:Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.ConfigureTestServices%2A no construtor de host. Para definir o escopo dos serviços substituídos para o próprio teste, 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 com escopo que retorna uma citação. A citação é integrada em um 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 marcação a seguir é 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 a injeção de serviço e de citação em um teste de integração, um serviço fictício é injetado no SUT pelo teste. O serviço fictício 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 entre aspas fornecido por TestQuoteService, portanto, 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 fictícia

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

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

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

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

No teste Get_SecurePageRedirectsAnUnauthenticatedUser, um WebApplicationFactoryClientOptions é definido para revogar a permissão de 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 esperado HttpStatusCode.Redirect, não o código de status final após o redirecionamento para a página de entrada, 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 a resposta final da página de entrada, em que o cabeçalho Location não estaria presente.

O aplicativo de teste pode simular um AuthenticationHandler<TOptions> no ConfigureTestServices para testar aspectos 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, "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 TestScheme corresponda ao esquema que seu aplicativo espera. 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, confira a seção Opções do cliente.

Testes básicos para middlewares de autenticação

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

Definir o ambiente

Defina o ambiente na fábrica de aplicativos personalizada:

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 de conteúdo do aplicativo

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

Desabilitar a 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 seus testes dependerem do carregamento de arquivos relativos a 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 xunit.runner.json no diretório do projeto de teste, com a configuração correta:

{
  "shadowCopy": false
}

Descarte de objetos

Depois que os testes da implementação de IClassFixture são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory. Se os objetos instanciados pelo desenvolvedor exigirem descarte, descarte-os na IClassFixture implementação. Para saber mais, confira Implementação de um método Dispose.

Exemplo de testes de integração

O aplicativo de exemplo é composto por dois aplicativos:

Aplicativo Diretório do projeto Descrição
Aplicativo de mensagens (o SUT) src/RazorPagesProject Permite que o usuário adicione, exclua uma ou todas as mensagens e as analise.
Aplicativo de teste tests/RazorPagesProject.Tests Usado para fazer o teste de integração do SUT.

Os testes podem ser executados usando os recursos de teste nativos do IDE, como o Visual Studio. Se estiver usando o Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando na pasta tests/RazorPagesProject.Tests:

dotnet test

Organização do aplicativo de mensagens (SUT)

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

  • A página de Í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 (número médio de palavras por mensagem).
  • Uma mensagem é descrita pela classe Message (Data/Message.cs) com duas propriedades: Id (chave) e Text (mensagem). A propriedade Text é necessá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 DAL (camada de acesso a dados) 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 repositório 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 do EF, Teste com 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 do repositório e não seja um exemplo eficaz do padrão UoW (Unidade de Trabalho), o Razor Pages dá suporte a esses padrões de desenvolvimento. Para obter mais informações, confira Criando a camada de persistência da infraestrutura e Lógica do controlador de teste (o exemplo implementa o padrão do repositório).

Organização do aplicativo de teste

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

Diretório do aplicativo de teste Descrição
AuthTests Contém métodos de teste para:
  • Acessar uma página segura por um usuário não autenticado.
  • Acessando uma página segura por um usuário autenticado com um AuthenticationHandler<TOptions> fictício.
  • Obter um perfil de usuário do GitHub e verificar o logon do 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 de Índice usando a classe personalizada WebApplicationFactory .
Helpers/Utilities
  • Utilities.cs contém o método InitializeDbForTests usado para propagar o banco de dados com dados de teste.
  • HtmlHelpers.cs fornece um método para retornar um AngleSharp IHtmlDocument para uso pelos métodos de teste.
  • HttpClientExtensions.cs forneça sobrecargas para que SendAsync envie solicitações para o 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 do 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 exige a exclusão de um registro do banco de dados; portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão tenha êxito.

O aplicativo de exemplo propaga 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 de banco de dados do SUT é registrado em Program.cs. O retorno de chamada builder.ConfigureServices do aplicativo de teste é executado depois que 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