Testes de integração no ASP.NET Core
Por Jos van der Til, Martin Costelloe Javier Calvarro Nelson.
Os testes de integração garantem que os componentes de um aplicativo funcionem corretamente em um nível que inclua a infraestrutura de suporte do aplicativo, como o banco de dados, o sistema de arquivos e a rede. O ASP.NET Core suporta testes de integração usando uma estrutura de teste de unidade com um host de teste e um servidor de teste na memória.
Este artigo pressupõe uma compreensão básica de testes de unidade. Se não estiver familiarizado com conceitos de teste, consulte o artigo Teste de Unidade no .NET Core e no .NET Standard e seu conteúdo associado.
Visualizar ou baixar código de exemplo (como transferir)
A aplicação de exemplo é uma aplicação Razor Páginas e pressupõe um conhecimento básico de Razor Páginas. Se não estiver familiarizado com o Razor Pages, consulte os seguintes artigos:
Para testar SPAs, recomendamos uma ferramenta como Playwright for .NET, que pode automatizar um navegador.
Introdução aos testes de integração
Os testes de integração avaliam os componentes de um aplicativo em um nível mais amplo do que testes de unidade. Os testes de unidade são usados para testar componentes de software isolados, como métodos de classe individuais. Os testes de integração confirmam que dois ou mais componentes do aplicativo trabalham juntos para produzir um resultado esperado, possivelmente incluindo todos os componentes necessários para processar totalmente uma solicitação.
Esses testes mais amplos são usados para testar a infraestrutura e toda a estrutura do aplicativo, geralmente incluindo os seguintes componentes:
- Base de dados
- Sistema de ficheiros
- Dispositivos de rede
- Pipeline de solicitação-resposta
Os testes de unidade usam componentes fabricados, conhecidos como falsos ou objetos falsificados, no lugar de componentes de infraestrutura.
Em contraste com os testes de unidade, os testes de integração:
- Use os componentes reais que o aplicativo usa na produção.
- Exigem mais código e processamento de dados.
- Leve mais tempo para correr.
Portanto, limite o uso de testes de integração aos cenários de infraestrutura mais importantes. Se um comportamento puder ser testado usando um teste de unidade ou um teste de integração, escolha o teste de unidade.
Em discussões de testes de integração, o projeto testado é frequentemente chamado de System Under Test, ou "SUT" para abreviar. "SUT" é usado ao longo deste artigo para se referir ao aplicativo ASP.NET Core que está sendo testado.
Não escreva testes de integração para cada de permutação de dados e acesso a arquivos com bancos de dados e sistemas de arquivos. Independentemente de quantos lugares em um aplicativo interagem com bancos de dados e sistemas de arquivos, um conjunto focado de testes de integração de leitura, gravação, atualização e exclusão geralmente é capaz de testar adequadamente os componentes do banco de dados e do sistema de arquivos. Use testes de unidade para testes de rotina de lógica de método que interagem com esses componentes. Em testes de unidade, o uso de falsificações ou simulações de infraestrutura resulta em execução de teste mais rápida.
ASP.NET Testes de integração principais
Os testes de integração no ASP.NET Core requerem o seguinte:
- Um projeto de teste é usado para conter e executar os testes. O projeto de teste tem uma referência ao SUT.
- O projeto de teste cria um host de teste para o SUT e usa um cliente de servidor de teste para lidar com solicitações e respostas com o SUT.
- Um corredor de teste é usado para executar os testes e relatar os resultados do teste.
Os testes de integração seguem uma sequência de eventos que incluem as etapas usuais de teste Preparar, Agire Asserir:
- A hospedagem web da SUT está configurada.
- Um cliente de servidor de teste é criado para enviar solicitações ao aplicativo.
- A etapa de teste Organizar é executada: o aplicativo de teste prepara uma solicitação.
- A etapa de teste do Act é executada: o cliente envia a solicitação e recebe a resposta.
- A etapa de teste Assert é executada: A resposta real é validada como uma de aprovação ou falha com base em uma resposta esperada.
- O processo continua até que todos os testes sejam executados.
- Os resultados dos testes são divulgados.
Normalmente, o host da Web de teste é configurado de forma diferente do host normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usados para os testes.
Os componentes de infraestrutura, como o host da Web de teste e o servidor de teste na memória (TestServer), são fornecidos ou gerenciados pelo pacote Microsoft.AspNetCore.Mvc.Testing. O uso deste pacote simplifica a criação e execução de testes.
O pacote Microsoft.AspNetCore.Mvc.Testing
lida com as seguintes tarefas:
- Copia o arquivo de dependências (
.deps
) do SUT para o diretóriobin
do projeto de teste. - Define a raiz de conteúdo como a raiz do projeto do SUT para que arquivos estáticos e páginas/visualizações sejam encontrados quando os testes forem executados.
- Fornece a classe WebApplicationFactory, para simplificar o processo de configuração do SUT com
TestServer
.
A documentação de testes de unidade descreve como configurar um projeto de teste e uma ferramenta de execução de testes, juntamente com instruções detalhadas sobre como executar testes e recomendações sobre como nomear testes e classes de teste.
Separe os testes de unidade dos testes de integração em diferentes projetos. Separar os testes:
- Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
- Permite controlar qual conjunto de testes é executado.
Não há praticamente nenhuma diferença entre a configuração para testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença está na forma como os testes são nomeados. Em uma aplicação Razor Pages, os testes de pontos de extremidade de página geralmente são baseados no nome da classe do modelo de página (por exemplo, IndexPageTests
para testar a integração de componentes na página Índice). Em um aplicativo MVC, os testes geralmente são organizados por classes de controlador e nomeados após os controladores que eles testam (por exemplo, HomeControllerTests
para testar a integração de componentes para o controlador Home).
Pré-requisitos do aplicativo de teste
O projeto de ensaio deve:
- Faça referência ao pacote
Microsoft.AspNetCore.Mvc.Testing
. - Especifique o SDK da Web no arquivo de projeto (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Esses pré-requisitos podem ser vistos no aplicativo de exemplo . Inspecione o arquivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. A aplicação de exemplo usa a estrutura de teste xUnit e a biblioteca do analisador AngleSharp, portanto, a aplicação de exemplo também faz referência:
Em aplicativos que usam xunit.runner.visualstudio
versão 2.4.2 ou posterior, o projeto de teste deve fazer referência ao pacote Microsoft.NET.Test.Sdk
.
O Entity Framework Core também é usado nos testes. Consulte o arquivo de projeto no GitHub.
Ambiente SUT
Se o ambiente do do SUT não estiver definido, o ambiente padrão será Desenvolvimento.
Testes básicos com o padrão WebApplicationFactory
Exponha a classe Program
implicitamente definida para o projeto de teste seguindo um destes procedimentos:
Exponha tipos internos do aplicativo Web para o projeto de teste. Isso pode ser feito no arquivo do projeto SUT (
.csproj
):<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Torne a classe
Program
pública usando uma declaração parcial de classe:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
A aplicação de exemplo usa a abordagem de classe parcial
Program
.
WebApplicationFactory<TEntryPoint> é usado para criar um TestServer para os testes de integração.
TEntryPoint
é a classe de ponto de entrada do SUT, geralmente Program.cs
.
As classes de teste implementam uma interface de fixação de classe (IClassFixture
) para indicar que a classe contém testes e fornecer instâncias de objetos partilhados entre os testes na classe.
A seguinte classe de teste, BasicTests
, usa o WebApplicationFactory
para inicializar o SUT e providenciar um HttpClient a um método de teste, Get_EndpointsReturnSuccessAndCorrectContentType
. O método verifica se o código de status da resposta foi bem-sucedido (200-299) e o cabeçalho Content-Type
está text/html; charset=utf-8
para várias páginas do aplicativo.
CreateClient() cria uma instância de HttpClient
que segue automaticamente redirecionamentos e lida com cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
Por padrão, os cookies não essenciais não são preservados nas solicitações quando a política de consentimento do Regulamento Geral de Proteção de Dados está ativada. Para preservar cookies não essenciais, como os usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Cookies essenciais.
AngleSharp vs Application Parts
para verificações antifalsificação
Este artigo usa o analisador AngleSharp para lidar com as verificações antifalsificação carregando páginas e analisando o HTML. Para testar os pontos de extremidade das visualizações do controlador e do Razor Pages em um nível inferior, sem se preocupar com a forma como eles são renderizados no navegador, considere usar Application Parts
. A abordagem Application Parts injeta um controlador ou Razor Page no aplicativo que pode ser usado para fazer solicitações JSON para obter os valores necessários. Para obter mais informações, consulte o blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts e de repositório GitHub associado pelo Martin Costello.
Personalizar WebApplicationFactory
É possível criar a configuração do host da Web de forma independente das classes de teste, por meio da herança de WebApplicationFactory<TEntryPoint> para criar uma ou mais fábricas personalizadas.
Herdar de
WebApplicationFactory
e substituir ConfigureWebHost. O IWebHostBuilder permite a configuração da coleção de serviços comIWebHostBuilder.ConfigureServices
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(IDbContextOptionsConfiguration<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
A propagação do banco de dados no aplicativo de exemplo é executada pelo método
InitializeDbForTests
. O método é descrito no exemplo de testes de integração na seção : Organização da aplicação de teste.O contexto da base de dados da SUT está registado em
Program.cs
. O retorno de chamadabuilder.ConfigureServices
do aplicativo de teste é executado depois de o códigoProgram.cs
do aplicativo é executado. Para usar um banco de dados diferente nos testes em vez do banco de dados do aplicativo, o contexto do banco de dados do aplicativo deve ser substituído embuilder.ConfigureServices
.O aplicativo de exemplo localiza o descritor de serviço para o contexto do banco de dados e usa o descritor para remover o registro de serviço. Em seguida, a fábrica adiciona um novo
ApplicationDbContext
que usa um banco de dados na memória para os testes.Para se conectar a um banco de dados diferente, altere o
DbConnection
. Para usar um banco de dados de teste do SQL Server:- Faça referência ao pacote NuGet
Microsoft.EntityFrameworkCore.SqlServer
no arquivo de projeto. - Ligue para
UseInMemoryDatabase
.
- Faça referência ao pacote NuGet
Utilize o
CustomWebApplicationFactory
personalizado em classes de teste. O exemplo a seguir usa a fábrica na classeIndexPageTests
:public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
O cliente do aplicativo de exemplo está configurado para impedir que o
HttpClient
siga redirecionamentos. Conforme explicado mais adiante na seção de autenticação simulada, isso permite que os testes verifiquem o resultado da primeira resposta do aplicativo. A primeira resposta é um redirecionamento em muitos desses testes com um cabeçalhoLocation
.Um teste típico usa os métodos
HttpClient
e auxiliar para processar a solicitação e a resposta:[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
Qualquer solicitação POST ao SUT deve passar pela verificação de antifalsificação feita automaticamente pelo sistema de proteção de dados contra falsificação do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:
- Faça um pedido para a página.
- Analise o cookie antifalsificação e solicite o token de validação da resposta.
- Faça a solicitação POST com o antifalsificação cookie e solicite o token de validação em vigor.
Os métodos de extensão auxiliar SendAsync
(Helpers/HttpClientExtensions.cs
) e o método auxiliar GetDocumentAsync
(Helpers/HtmlHelpers.cs
) na aplicação de exemplo usam o analisador AngleSharp para manipular a verificação antifalsificação com os seguintes métodos:
-
GetDocumentAsync
: Recebe o HttpResponseMessage e devolve umIHtmlDocument
.GetDocumentAsync
usa uma fábrica que prepara uma resposta virtual com base noHttpResponseMessage
original. Para obter mais informações, consulte a documentação do AngleSharp. -
SendAsync
métodos de extensão para oHttpClient
compõem um HttpRequestMessage e chamam SendAsync(HttpRequestMessage) para enviar solicitações ao SUT. Sobrecargas paraSendAsync
aceitam o formulário HTML (IHtmlFormElement
) e o seguinte:- Botão Enviar do formulário (
IHtmlElement
) - Coleção de valores de formulário (
IEnumerable<KeyValuePair<string, string>>
) - Botão Enviar (
IHtmlElement
) e valores de formulário (IEnumerable<KeyValuePair<string, string>>
)
- Botão Enviar do formulário (
AngleSharp é uma biblioteca de análises sintáticas de terceiros usada para fins de demonstração neste artigo e na aplicação de exemplo. O AngleSharp não é suportado ou necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o Html Agility Pack (HAP). Outra abordagem é escrever código para lidar com o token de verificação de solicitação do sistema antifalsificação e o antifalsificação cookie diretamente. Consulte AngleSharp vs Application Parts
para verificações antifalsificação neste artigo para obter mais informações.
O EF-Core provedor de banco de dados na memória pode ser usado para testes limitados e básicos, no entanto, o provedor SQLite é a escolha recomendada para testes na memória.
Consulte Estender a Inicialização com Filtros de Inicialização, que mostra como configurar middleware usando IStartupFilter, o que é útil quando um teste requer um serviço ou middleware personalizado.
Personalize o cliente com WithWebHostBuilder
Quando uma configuração adicional é necessária dentro de um método de teste, WithWebHostBuilder cria um novo WebApplicationFactory
com um IWebHostBuilder que é ainda mais personalizado pela configuração.
O código de exemplo chama WithWebHostBuilder
para substituir serviços configurados por stubs de teste. Para obter mais informações e exemplos de uso, consulte de serviços simulados de injeção neste artigo.
O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot
do aplicativo de exemplo demonstra o uso de WithWebHostBuilder
. Este teste executa uma exclusão de registro no banco de dados acionando um envio de formulário no SUT.
Como outro teste na classe IndexPageTests
executa uma operação que exclui todos os registros no banco de dados e pode ser executado antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot
, o banco de dados é repropagado nesse método de teste para garantir que um registro esteja presente para o SUT excluir. A seleção do primeiro botão de exclusão do formulário messages
no SUT é simulada na solicitação ao SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Opções do cliente
Consulte a página WebApplicationFactoryClientOptions para obter os padrões e as opções disponíveis ao criar instâncias HttpClient
.
Crie a classe WebApplicationFactoryClientOptions
e passe-a para o método CreateClient():
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
NOTA: Para evitar avisos de redirecionamento HTTPS em logs ao usar o middleware de redirecionamento HTTPS, defina BaseAddress = new Uri("https://localhost")
Injetar serviços simulados
Os serviços podem ser substituídos em um teste com uma chamada para ConfigureTestServices no construtor de hosts. Para definir o escopo dos serviços substituídos para o teste em si, o método WithWebHostBuilder é usado para recuperar um host builder. Isso pode ser visto nos seguintes testes:
- Get_QuoteService_ProvidesQuoteInPage
- Obter_PáginaDePerfilGithubPodeObterUmUtilizadorGithub
- A_PáginaSeguraÉRetornadaParaUmUtilizadorAutenticado
O SUT de exemplo inclui um serviço com um escopo definido que retorna uma citação. A cotação é incorporada em um campo oculto na página Índice quando a página Índice é solicitada.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
A seguinte marcação é gerada quando o aplicativo SUT é executado:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Para testar o serviço e a injeção de cotação em um teste de integração, um serviço simulado é injetado no SUT pelo teste. O serviço simulado substitui o QuoteService
do aplicativo por um serviço fornecido pelo aplicativo de teste, chamado TestQuoteService
:
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
Quando o ConfigureTestServices
é chamado, o serviço com escopo é registado.
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
A marcação gerada durante a execução do teste reflete o texto da citação citado por TestQuoteService
, portanto, a asserção é bem-sucedida.
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Autenticação simulada
Os testes na classe AuthTests
verificam se um endpoint seguro:
- Redireciona um usuário não autenticado para a página de login do aplicativo.
- Retorna conteúdo para um usuário autenticado.
No SUT, a página /SecurePage
utiliza uma convenção denominada AuthorizePage para aplicar um elemento AuthorizeFilter à página. Para obter mais informações, consulte as convenções de autorização das páginas Razor.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
No teste de Get_SecurePageRedirectsAnUnauthenticatedUser
, um WebApplicationFactoryClientOptions é definido para não permitir redirecionamentos definindo AllowAutoRedirect como false
:
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
Ao não permitir que o cliente siga o redirecionamento, as seguintes verificações podem ser feitas:
- O código de status retornado pelo SUT pode ser verificado em relação ao resultado HttpStatusCode.Redirect esperado, não o código de status final após o redirecionamento para a página de login, que seria HttpStatusCode.OK.
- O valor do cabeçalho
Location
nos cabeçalhos de resposta é verificado para confirmar que começa comhttp://localhost/Identity/Account/Login
, e não com a resposta final da página de início de sessão, onde o cabeçalhoLocation
não estaria presente.
O aplicativo de teste pode simular um AuthenticationHandler<TOptions> em ConfigureTestServices para testar aspetos de autenticação e autorização. Um cenário mínimo retorna AuthenticateResult.Success:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
O TestAuthHandler
é chamado para autenticar um usuário quando o esquema de autenticação é definido como TestScheme
onde AddAuthentication
está registrado para ConfigureTestServices
. É importante que o esquema de TestScheme
corresponda ao esquema esperado pelo seu aplicativo. Caso contrário, a autenticação não funcionará.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Para obter mais informações sobre WebApplicationFactoryClientOptions
, consulte a seção opções do cliente.
Testes básicos para middleware de autenticação
Consulte este repositório no GitHub para testes básicos de middleware de autenticação. Ele contém um servidor de teste que é específico para o cenário de teste.
Definir o ambiente
Defina o ambiente na fábrica de aplicações personalizadas:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
Como a infraestrutura de teste infere o caminho raiz do conteúdo do aplicativo
O construtor WebApplicationFactory
infere o caminho da raiz de conteúdo da aplicação procurando um WebApplicationFactoryContentRootAttribute no assembly que contém os testes de integração com uma chave igual ao assembly TEntryPoint
System.Reflection.Assembly.FullName
. Caso um atributo com a chave correta não seja encontrado, WebApplicationFactory
volta a procurar por um arquivo de solução (.sln) e acrescenta o nome do assembly TEntryPoint
ao diretório da solução. O diretório raiz do aplicativo (o caminho da raiz do conteúdo) é usado para descobrir exibições e arquivos de conteúdo.
Desativar cópia de sombra
A cópia de sombra faz com que os testes sejam executados em um diretório diferente do diretório de saída. Se os testes dependerem do carregamento de arquivos relativos ao Assembly.Location
e você encontrar problemas, talvez seja necessário desabilitar a cópia de sombra.
Para desativar a cópia de sombras ao usar xUnit, crie um arquivo de xunit.runner.json
no diretório do projeto de teste, com a configuração correta .
{
"shadowCopy": false
}
Eliminação de objetos
Depois que os testes da implementação do IClassFixture
são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory
. Se os objetos instanciados pelo desenvolvedor precisarem ser eliminados, elimine-os na implementação IClassFixture
. Para obter mais informações, consulte Implementing a Dispose method.
Exemplo de testes de integração
O de aplicativo de exemplo é composto por dois aplicativos:
Aplicação | Diretório de projetos | Descrição |
---|---|---|
Aplicativo de mensagem (o SUT) | src/RazorPagesProject |
Permite que um usuário adicione, exclua um, exclua todos e analise mensagens. |
Aplicativo de teste | tests/RazorPagesProject.Tests |
Usado para testar a integração do SUT. |
Os testes podem ser executados usando os recursos de teste internos de um IDE, como Visual Studio. Se estiver usando Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando no diretório tests/RazorPagesProject.Tests
:
dotnet test
Organização do aplicativo de mensagem (SUT)
O SUT é um sistema de mensagens Razor Pages com as seguintes características:
- A página Índice do aplicativo (
Pages/Index.cshtml
ePages/Index.cshtml.cs
) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (média de palavras por mensagem). - Uma mensagem é descrita pela classe
Message
(Data/Message.cs
) com duas propriedades:Id
(chave) eText
(mensagem). A propriedadeText
é obrigatória e limitada a 200 caracteres. - As mensagens são armazenadas usando o banco de dados na memória do Entity Framework †.
- O aplicativo contém uma camada de acesso a dados (DAL) em sua classe de contexto de banco de dados,
AppDbContext
(Data/AppDbContext.cs
). - Se o banco de dados estiver vazio na inicialização do aplicativo, o armazenamento de mensagens será inicializado com três mensagens.
- O aplicativo inclui um
/SecurePage
que só pode ser acessado por um usuário autenticado.
†O artigo do EF, Test with InMemory, explica como usar um banco de dados na memória para testes com o MSTest. Este tópico usa o xUnit estrutura de teste. Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.
Embora o aplicativo não use o padrão de repositório e não seja um exemplo eficaz do padrão de Unidade de Trabalho (UoW), o Razor Pages oferece suporte a esses padrões de desenvolvimento. Para mais informações, consulte Conceção da camada de persistência da infraestrutura e Lógica do controlador de testes (o exemplo implementa o padrão de repositório).
Testar a organização do aplicativo
O aplicativo de teste é um aplicativo de console dentro do diretório tests/RazorPagesProject.Tests
.
Diretório de aplicativos de teste | Descrição |
---|---|
AuthTests |
Contém métodos de teste para:
|
BasicTests |
Contém um método de teste para roteamento e tipo de conteúdo. |
IntegrationTests |
Contém os testes de integração para a página Índice usando a classe WebApplicationFactory personalizada. |
Helpers/Utilities |
|
A estrutura de teste é xUnit. Os testes de integração são realizados usando o Microsoft.AspNetCore.TestHost, que inclui o TestServer. Como o pacote Microsoft.AspNetCore.Mvc.Testing
é usado para configurar o host de teste e o servidor de teste, os pacotes TestHost
e TestServer
não exigem referências diretas de pacote no arquivo de projeto do aplicativo de teste ou na configuração do desenvolvedor no aplicativo de teste.
Os testes de integração geralmente exigem um pequeno conjunto de dados no banco de dados antes da execução do teste. Por exemplo, um teste de exclusão chama para uma exclusão de registro de banco de dados, portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão seja bem-sucedida.
O aplicativo de exemplo semeia o banco de dados com três mensagens em Utilities.cs
que os testes podem usar quando são executados:
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
O contexto da base de dados da SUT está registado em Program.cs
. O retorno de chamada builder.ConfigureServices
do aplicativo de teste é executado depois de o código Program.cs
do aplicativo é executado. Para usar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices
. Para mais informações, consulte a seção Personalizar WebApplicationFactory .
Recursos adicionais
Este tópico pressupõe uma compreensão básica de testes de unidade. Se não estiver familiarizado com conceitos de teste, consulte o tópico Teste de unidade de no .NET Core e no .NET Standard e seu conteúdo vinculado.
Ver ou baixar código de exemplo (como fazer a transferência)
A aplicação de exemplo é uma aplicação Razor Pages e exige um conhecimento básico de Razor Pages. Se não estiver familiarizado com o Razor Pages, consulte os seguintes tópicos:
Observação
Para testar SPAs, recomendamos uma ferramenta como o Playwright for .NET, que pode automatizar um navegador.
Introdução aos testes de integração
Os testes de integração avaliam os componentes de uma aplicação num nível mais amplo do que testes de unidade. Os testes de unidade são usados para testar componentes de software isolados, como métodos de classe individuais. Os testes de integração confirmam que dois ou mais componentes do aplicativo trabalham juntos para produzir um resultado esperado, possivelmente incluindo todos os componentes necessários para processar totalmente uma solicitação.
Esses testes mais amplos são usados para testar a infraestrutura e toda a estrutura do aplicativo, geralmente incluindo os seguintes componentes:
- Base de dados
- Sistema de ficheiros
- Dispositivos de rede
- Pipeline de resposta a pedidos
Os testes de unidade usam componentes fabricados, conhecidos como falsos ou objetos de simulação, no lugar de componentes de infraestrutura.
Em contraste com os testes de unidade, os testes de integração:
- Use os componentes reais que o aplicativo usa na produção.
- Exigem mais código e processamento de dados.
- Leve mais tempo para correr.
Portanto, limite o uso de testes de integração aos cenários de infraestrutura mais importantes. Se um comportamento puder ser testado usando um teste de unidade ou um teste de integração, escolha o teste de unidade.
Em discussões de testes de integração, o projeto testado é frequentemente chamado de System Under Test, ou "SUT" para abreviar. "SUT" é usado ao longo deste artigo para se referir ao aplicativo ASP.NET Core que está sendo testado.
Não escreva testes de integração para cada de permutação de dados e acesso a arquivos com bancos de dados e sistemas de arquivos. Independentemente de quantos lugares em um aplicativo interagem com bancos de dados e sistemas de arquivos, um conjunto focado de testes de integração de leitura, gravação, atualização e exclusão geralmente é capaz de testar adequadamente os componentes do banco de dados e do sistema de arquivos. Use testes de unidade para testes de rotina de lógica de método que interagem com esses componentes. Em testes de unidade, o uso de falsificações ou simulações de infraestrutura resulta em execução de teste mais rápida.
ASP.NET Testes de integração principais
Os testes de integração no ASP.NET Core requerem o seguinte:
- Um projeto de teste é usado para conter e executar os testes. O projeto de teste tem uma referência ao SUT.
- O projeto de teste cria um host de teste para o SUT e usa um cliente de servidor de teste para lidar com solicitações e respostas com o SUT.
- Um corredor de teste é usado para executar os testes e relatar os resultados do teste.
Os testes de integração seguem uma sequência de eventos que incluem as etapas usuais de teste Preparar, Executare Verificar:
- O anfitrião web da SUT está configurado.
- Um cliente de servidor de teste é criado para enviar solicitações ao aplicativo.
- A etapa de teste Arranjar é executada: a aplicação de teste prepara uma solicitação.
- A etapa de teste do Act é executada: o cliente envia a solicitação e recebe a resposta.
- A etapa de teste Assert é executada: A resposta real é validada como um êxito ou falha com base em uma resposta esperada.
- O processo continua até que todos os testes sejam executados.
- Os resultados dos testes foram divulgados.
Normalmente, o host da Web de teste é configurado de forma diferente do host normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usados para os testes.
Os componentes de infraestrutura, como o host da Web de teste e o servidor de teste na memória (TestServer), são fornecidos ou gerenciados pelo pacote de Microsoft.AspNetCore.Mvc.Testing. O uso deste pacote simplifica a criação e execução de testes.
O pacote Microsoft.AspNetCore.Mvc.Testing
lida com as seguintes tarefas:
- Copia o arquivo de dependências (
.deps
) do SUT para o diretóriobin
do projeto de teste. - Define a raiz de conteúdo para a raiz do projeto SUT para que arquivos estáticos e páginas/visualizações sejam encontrados quando os testes forem executados.
- Fornece a classe WebApplicationFactory para agilizar a inicialização do SUT com
TestServer
.
A documentação dos testes de unidade descreve como configurar um projeto de teste e um executante de testes, juntamente com instruções detalhadas sobre como executar testes e recomendações sobre como nomear testes e classes de teste.
Separe os testes de unidade dos testes de integração em diferentes projetos. Separar os testes:
- Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
- Permite controlar qual conjunto de testes é executado.
Não há praticamente nenhuma diferença entre a configuração para testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença está na forma como os testes são nomeados. Em um aplicativo Razor Pages, os testes de pontos de extremidade de página são geralmente nomeados de acordo com a classe de modelo de página (por exemplo, IndexPageTests
para testar a integração de componentes para página Índice). Em um aplicativo MVC, os testes geralmente são organizados por classes de controlador e nomeados após os controladores que eles testam (por exemplo, HomeControllerTests
para testar a integração de componentes para o controlador Home).
Pré-requisitos do aplicativo de teste
O projeto de ensaio deve:
- Faça referência ao pacote
Microsoft.AspNetCore.Mvc.Testing
. - Especifique o SDK da Web no arquivo de projeto (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Esses pré-requisitos podem ser vistos no aplicativo de exemplo . Inspecione o arquivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. O aplicativo de exemplo usa a estrutura de teste xUnit e a biblioteca de analisador AngleSharp, portanto, o aplicativo de exemplo também faz referência:
Em aplicativos que usam xunit.runner.visualstudio
versão 2.4.2 ou posterior, o projeto de teste deve fazer referência ao pacote Microsoft.NET.Test.Sdk
.
O Entity Framework Core também é usado nos testes. As referências do aplicativo:
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.InMemory
Microsoft.EntityFrameworkCore.Tools
Ambiente SUT
Se a de ambiente do SUT não estiver definida, o padrão do ambiente será Desenvolvimento.
Testes básicos com o padrão WebApplicationFactory
WebApplicationFactory<TEntryPoint> é usado para criar um TestServer para os testes de integração.
TEntryPoint
é a classe de ponto de entrada do SUT, geralmente a classe Startup
.
As classes de teste implementam uma interface de de fixação de classe (IClassFixture
) para indicar que a classe contém testes e fornecer instâncias de objeto compartilhado entre os testes na classe.
A seguinte classe de teste, BasicTests
, utiliza o WebApplicationFactory
para inicializar o SUT e fornecer um HttpClient a um método de teste, Get_EndpointsReturnSuccessAndCorrectContentType
. O método verifica se o código de status da resposta foi bem-sucedido (códigos de status no intervalo 200-299) e se o cabeçalho Content-Type
está text/html; charset=utf-8
para várias páginas do aplicativo.
CreateClient() cria uma instância de HttpClient
que segue automaticamente redirecionamentos e lida com cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;
public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
Por padrão, os cookies não essenciais não são preservados em todas as solicitações quando a política de consentimento GDPR está ativada. Para preservar cookies não essenciais, como os usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Cookies Essenciais.
Personalizar WebApplicationFactory
A configuração do anfitrião Web pode ser criada de forma independente das classes de teste, herdando de WebApplicationFactory
para criar uma ou mais fábricas personalizadas.
Herdar de
WebApplicationFactory
e substituir ConfigureWebHost. O IWebHostBuilder permite a configuração da coleção de serviços com ConfigureServices:public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var descriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(descriptor); services.AddDbContext<ApplicationDbContext>(options => { options.UseInMemoryDatabase("InMemoryDbForTesting"); }); var sp = services.BuildServiceProvider(); using (var scope = sp.CreateScope()) { var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService<ApplicationDbContext>(); var logger = scopedServices .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>(); db.Database.EnsureCreated(); try { Utilities.InitializeDbForTests(db); } catch (Exception ex) { logger.LogError(ex, "An error occurred seeding the " + "database with test messages. Error: {Message}", ex.Message); } } }); } }
A propagação do banco de dados no aplicativo de exemplo é executada pelo método
InitializeDbForTests
. O método é descrito na seção Integration tests sample: Test app organization seção.O contexto da base de dados do SUT é registado no seu método
Startup.ConfigureServices
. O retorno de chamadabuilder.ConfigureServices
do aplicativo de teste é executado depois de o códigoStartup.ConfigureServices
do aplicativo é executado. A ordem de execução é uma alteração de quebra para o Generic Host com o lançamento do ASP.NET Core 3.0. Para usar um banco de dados diferente do banco de dados do aplicativo para os testes, o contexto deste deve ser substituído embuilder.ConfigureServices
.Para SUTs que ainda usam o Web Host, o retorno de chamada
builder.ConfigureServices
do aplicativo de teste é executado antes de o códigoStartup.ConfigureServices
do SUT. O callbackbuilder.ConfigureTestServices
do aplicativo de teste é executado após.O aplicativo de exemplo localiza o descritor de serviço para o contexto do banco de dados e usa o descritor para remover o registro de serviço. Em seguida, a fábrica adiciona um novo
ApplicationDbContext
que usa um banco de dados na memória para os testes.Para se conectar a um banco de dados diferente do banco de dados na memória, altere a chamada
UseInMemoryDatabase
para conectar o contexto a um banco de dados diferente. Para usar um banco de dados de teste do SQL Server:- Faça referência ao pacote NuGet
Microsoft.EntityFrameworkCore.SqlServer
no arquivo de projeto. - Chame
UseSqlServer
com uma string de ligação para o banco de dados.
services.AddDbContext<ApplicationDbContext>((options, context) => { context.UseSqlServer( Configuration.GetConnectionString("TestingDbConnectionString")); });
- Faça referência ao pacote NuGet
Use o
CustomWebApplicationFactory
personalizado em classes de teste. O exemplo a seguir usa a fábrica na classeIndexPageTests
:public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> _factory; public IndexPageTests( CustomWebApplicationFactory<RazorPagesProject.Startup> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
O cliente do aplicativo de exemplo está configurado para impedir que o
HttpClient
siga redirecionamentos. Conforme explicado mais adiante na seção de autenticação simulada, isso permite que os testes verifiquem o resultado da primeira resposta do aplicativo. A primeira resposta é um redirecionamento em muitos desses testes com um cabeçalhoLocation
.Um teste típico usa os métodos
HttpClient
e auxiliar para processar a solicitação e a resposta:[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
Qualquer solicitação POST ao SUT deve satisfazer a verificação antifalsificação feita automaticamente pelo sistema antifalsificação de proteção de dados do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:
- Faça um pedido para a página.
- Analise o cookie antifalsificação e solicite o token de validação da resposta.
- Faça a solicitação POST com o antifalsificação cookie e com o token de validação de solicitações devidamente posicionado.
Os métodos de extensão auxiliar SendAsync
(Helpers/HttpClientExtensions.cs
) e o método auxiliar de GetDocumentAsync
(Helpers/HtmlHelpers.cs
) no aplicativo de exemplo usam o analisador de AngleSharp para manipular a verificação antifalsificação com os seguintes métodos:
-
GetDocumentAsync
: Recebe o HttpResponseMessage e devolve umIHtmlDocument
.GetDocumentAsync
usa uma fábrica que prepara uma resposta virtual com base noHttpResponseMessage
original. Para obter mais informações, consulte a documentação do AngleSharp. -
SendAsync
métodos de extensão para oHttpClient
compõem um HttpRequestMessage e chamam SendAsync(HttpRequestMessage) para enviar solicitações ao SUT. As sobrecargas deSendAsync
aceitam o formulário HTML (IHtmlFormElement
) e o seguinte:- Botão Enviar do formulário (
IHtmlElement
) - Coleção de valores de formulário (
IEnumerable<KeyValuePair<string, string>>
) - Botão Enviar (
IHtmlElement
) e valores de formulário (IEnumerable<KeyValuePair<string, string>>
)
- Botão Enviar do formulário (
Observação
AngleSharp é uma biblioteca de análise de terceiros usada para fins de demonstração neste tópico e no aplicativo de exemplo. O AngleSharp não é suportado ou necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o Html Agility Pack (HAP). Outra abordagem é escrever código para lidar diretamente com o token de verificação de solicitação do sistema antifalsificação e o token antifalsificação cookie.
Observação
O provedor de banco de dados na memória EF-Core pode ser usado para testes limitados e básicos, no entanto, o provedor SQLite é a escolha recomendada para testes em memória.
Personalize o cliente com WithWebHostBuilder
Quando uma configuração adicional é necessária dentro de um método de teste, WithWebHostBuilder cria um novo WebApplicationFactory
com um IWebHostBuilder que é ainda mais personalizado pela configuração.
O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot
do aplicativo de exemplo demonstra o uso de WithWebHostBuilder
. Este teste executa uma exclusão de registro no banco de dados acionando um envio de formulário no SUT.
Como outro teste na classe IndexPageTests
executa uma operação que exclui todos os registros no banco de dados e pode ser executado antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot
, o banco de dados é repropagado nesse método de teste para garantir que um registro esteja presente para o SUT excluir. A seleção do primeiro botão de exclusão do formulário messages
no SUT é simulada na solicitação ao SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices
.GetRequiredService<ApplicationDbContext>();
var logger = scopedServices
.GetRequiredService<ILogger<IndexPageTests>>();
try
{
Utilities.ReinitializeDbForTests(db);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding " +
"the database with test messages. Error: {Message}",
ex.Message);
}
}
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Opções do cliente
A tabela a seguir mostra o WebApplicationFactoryClientOptions padrão disponível ao criar instâncias HttpClient
.
Opção | Descrição | Padrão |
---|---|---|
AllowAutoRedirect | Obtém ou define se as instâncias HttpClient devem ou não seguir automaticamente as respostas de redirecionamento. |
true |
BaseAddress | Obtém ou define o endereço base das instâncias de HttpClient . |
http://localhost |
HandleCookies | Obtém ou define se HttpClient instâncias devem lidar com cookies. |
true |
MaxAutomaticRedirections | Obtém ou define o número máximo de respostas de redirecionamento que HttpClient instâncias devem seguir. |
7 |
Crie a classe WebApplicationFactoryClientOptions
e passe-a para o método CreateClient() (os valores padrão são mostrados no exemplo de código):
// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;
_client = _factory.CreateClient(clientOptions);
Injetar serviços simulados
Os serviços podem ser substituídos em um teste com uma chamada para ConfigureTestServices no construtor de hosts.
Para injetar serviços simulados, o SUT deve ter uma classe Startup
com um método Startup.ConfigureServices
.
O SUT de exemplo inclui um serviço com escopo definido que retorna uma cotação. A citação é incorporada em um campo escondido na página de índice quando a página de índice é solicitada.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Startup.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
A seguinte marcação é gerada quando o aplicativo SUT é executado:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Para testar o serviço e a injeção de cotação em um teste de integração, um serviço simulado é injetado no SUT pelo teste. O serviço simulado substitui o QuoteService
do aplicativo por um serviço fornecido pelo aplicativo de teste, chamado TestQuoteService
:
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
é chamado e o serviço com escopo é registrado:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
A marcação produzida durante a execução do teste reflete o texto da citação fornecido por TestQuoteService
; assim, a asserção é bem-sucedida.
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Autenticação simulada
Os testes na classe AuthTests
verificam se um endpoint seguro:
- Redireciona um usuário não autenticado para a página de login do aplicativo.
- Retorna conteúdo para um usuário autenticado.
No SUT, a página /SecurePage
usa uma convenção AuthorizePage para aplicar um AuthorizeFilter à página. Para mais informações, consulte as Convenções de autorização das Páginas Razor.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
No teste de Get_SecurePageRedirectsAnUnauthenticatedUser
, um WebApplicationFactoryClientOptions é definido para não permitir redirecionamentos definindo AllowAutoRedirect como false
:
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
Ao não permitir que o cliente siga o redirecionamento, as seguintes verificações podem ser feitas:
- O código de status retornado pelo SUT pode ser verificado em relação ao resultado HttpStatusCode.Redirect esperado, não o código de status final após o redirecionamento para a página de login, que seria HttpStatusCode.OK.
- O valor do cabeçalho
Location
nos cabeçalhos de resposta é verificado para confirmar que ele começa comhttp://localhost/Identity/Account/Login
, não com a resposta final da página de login, onde o cabeçalhoLocation
não estaria presente.
O aplicativo de teste pode simular um AuthenticationHandler<TOptions> em ConfigureTestServices para testar aspetos de autenticação e autorização. Um cenário mínimo retorna um AuthenticateResult.Success:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
O TestAuthHandler
é chamado para autenticar um usuário quando o esquema de autenticação é definido como Test
onde AddAuthentication
está registrado para ConfigureTestServices
. É importante que o esquema de Test
corresponda ao esquema esperado pelo seu aplicativo. Caso contrário, a autenticação não funcionará.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"Test", options => {});
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Test");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Para obter mais informações sobre WebApplicationFactoryClientOptions
, consulte a seção Opções do Cliente de.
Definir o ambiente
Por padrão, o ambiente de host e aplicativo do SUT é configurado para usar o ambiente de desenvolvimento. Para sobrescrever o ambiente do SUT quando usar IHostBuilder
:
- Defina a variável de ambiente
ASPNETCORE_ENVIRONMENT
(por exemplo,Staging
,Production
ou outro valor personalizado, comoTesting
). - Substitua
CreateHostBuilder
no aplicativo de teste para ler variáveis de ambiente prefixadas comASPNETCORE
.
protected override IHostBuilder CreateHostBuilder() =>
base.CreateHostBuilder()
.ConfigureHostConfiguration(
config => config.AddEnvironmentVariables("ASPNETCORE"));
Se o SUT usar o host da Web (IWebHostBuilder
), substitua CreateWebHostBuilder
:
protected override IWebHostBuilder CreateWebHostBuilder() =>
base.CreateWebHostBuilder().UseEnvironment("Testing");
Como a infraestrutura de teste infere o caminho raiz do conteúdo do aplicativo
O construtor WebApplicationFactory
infere o caminho da raiz de conteúdo da aplicação , procurando um WebApplicationFactoryContentRootAttribute no assembly que contém os testes de integração, com uma chave igual à do assembly TEntryPoint
System.Reflection.Assembly.FullName
. Caso um atributo com a chave correta não seja encontrado, WebApplicationFactory
reverte para procurar um arquivo de solução (.sln) e acrescenta o nome do assembly TEntryPoint
ao diretório da solução. O diretório raiz do aplicativo (o caminho da raiz do conteúdo) é usado para descobrir exibições e arquivos de conteúdo.
Desativar cópia de sombra
A cópia de sombra faz com que os testes sejam executados em um diretório diferente do diretório de saída. Se os testes dependerem do carregamento de ficheiros relativos ao Assembly.Location
e encontrardes problemas, talvez seja necessário desativar a cópia de sombra.
Para desabilitar a cópia de sombra ao usar xUnit, crie um arquivo de xunit.runner.json
no diretório do projeto de teste, com a definição de configuração correta:
{
"shadowCopy": false
}
Eliminação de objetos
Depois que os testes da implementação do IClassFixture
são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory
. Se os objetos instanciados pelo desenvolvedor precisarem ser eliminados, elimine-os na implementação IClassFixture
. Para obter mais informações, consulte Implementing a Dispose method.
Exemplo de testes de integração
O de aplicativo de exemplo é composto por dois aplicativos:
Aplicação | Diretório de projetos | Descrição |
---|---|---|
Aplicativo de mensagem (o SUT) | src/RazorPagesProject |
Permite que um usuário adicione, exclua um, exclua todos e analise mensagens. |
Aplicativo de teste | tests/RazorPagesProject.Tests |
Usado para testar a integração do SUT. |
Os testes podem ser executados usando os recursos de teste internos de um IDE, como Visual Studio. Se estiver usando Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando no diretório tests/RazorPagesProject.Tests
:
dotnet test
Organização do aplicativo de mensagem (SUT)
O SUT é um sistema de mensagens Razor Pages com as seguintes características:
- A página Índice do aplicativo (
Pages/Index.cshtml
ePages/Index.cshtml.cs
) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (média de palavras por mensagem). - Uma mensagem é descrita pela classe
Message
(Data/Message.cs
) com duas propriedades:Id
(chave) eText
(mensagem). A propriedadeText
é obrigatória e limitada a 200 caracteres. - As mensagens são armazenadas usando a base de dados em memória do Entity Framework †.
- O aplicativo contém uma camada de acesso a dados (DAL) em sua classe de contexto de banco de dados,
AppDbContext
(Data/AppDbContext.cs
). - Se o banco de dados estiver vazio na inicialização do aplicativo, o armazenamento de mensagens será inicializado com três mensagens.
- O aplicativo inclui um
/SecurePage
que só pode ser acessado por um usuário autenticado.
†O tópico EF, Test with InMemory, explica como usar um banco de dados na memória para testes com MSTest. Este tópico usa a estrutura de teste xUnit . Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.
Embora o aplicativo não use o padrão de repositório e não seja um exemplo eficaz do padrão de Unidade de Trabalho (UoW), o Razor Pages oferece suporte a esses padrões de desenvolvimento. Para mais informações, consulte Planear a camada de persistência da infraestrutura e Testar a lógica do controlador (o exemplo implementa o padrão de repositório).
Testar a organização do aplicativo
O aplicativo de teste é um aplicativo de console dentro do diretório tests/RazorPagesProject.Tests
.
Diretório de aplicativos de teste | Descrição |
---|---|
AuthTests |
Contém métodos de teste para:
|
BasicTests |
Contém um método de teste para roteamento e tipo de conteúdo. |
IntegrationTests |
Contém os testes de integração para a página Índice usando a classe WebApplicationFactory personalizada. |
Helpers/Utilities |
|
A estrutura de teste é xUnit. Os testes de integração são realizados usando o Microsoft.AspNetCore.TestHost, que inclui o TestServer. Como o pacote Microsoft.AspNetCore.Mvc.Testing
é usado para configurar o host de teste e o servidor de teste, os pacotes TestHost
e TestServer
não exigem referências diretas de pacote no arquivo de projeto do aplicativo de teste ou na configuração do desenvolvedor no aplicativo de teste.
Os testes de integração geralmente exigem um pequeno conjunto de dados no banco de dados antes da execução do teste. Por exemplo, um teste de exclusão chama para uma exclusão de registro de banco de dados, portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão seja bem-sucedida.
O aplicativo de exemplo semeia o banco de dados com três mensagens em Utilities.cs
que os testes podem usar quando são executados:
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
O contexto da base de dados do SUT é registado no seu método Startup.ConfigureServices
. A função de retorno builder.ConfigureServices
do aplicativo de teste é chamada depois de o código Startup.ConfigureServices
do aplicativo ser executado. Para usar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices
. Para obter mais informações, consulte a seção Personalizar WebApplicationFactory.
Para SUTs que ainda usam o Web Host, o retorno de chamada builder.ConfigureServices
do aplicativo de teste é executado antes de o código Startup.ConfigureServices
do SUT. O retorno de chamada builder.ConfigureTestServices
do aplicativo de teste é executado após.
Recursos adicionais
Este artigo pressupõe uma compreensão básica de testes de unidade. Se não estiver familiarizado com conceitos de teste, consulte o artigo Teste de unidade de no .NET Core e no .NET Standard e seu conteúdo vinculado.
Exibir ou baixar código de exemplo (como descarregar)
A aplicação de exemplo é uma aplicação Razor Pages e assume uma compreensão básica de Razor Pages. Se não estiver familiarizado com o Razor Pages, consulte os seguintes artigos:
Para testar SPAs, recomendamos uma ferramenta como Playwright for .NET, que pode automatizar um navegador.
Introdução aos testes de integração
Os testes de integração avaliam os componentes de um aplicativo em um nível mais amplo do que testes de unidade. Os testes de unidade são usados para testar componentes de software isolados, como métodos de classe individuais. Os testes de integração confirmam que dois ou mais componentes do aplicativo trabalham juntos para produzir um resultado esperado, possivelmente incluindo todos os componentes necessários para processar totalmente uma solicitação.
Esses testes mais amplos são usados para testar a infraestrutura e toda a estrutura do aplicativo, geralmente incluindo os seguintes componentes:
- Base de dados
- Sistema de ficheiros
- Dispositivos de rede
- Pipeline de solicitação-resposta
Os testes de unidade usam componentes fabricados, conhecidos como objetos falsos ou objetos simulados, no lugar de componentes de infraestrutura.
Em contraste com os testes de unidade, os testes de integração:
- Use os componentes reais que o aplicativo usa na produção.
- Exigem mais código e processamento de dados.
- Leve mais tempo para correr.
Portanto, limite o uso de testes de integração aos cenários de infraestrutura mais importantes. Se um comportamento puder ser testado usando um teste de unidade ou um teste de integração, escolha o teste de unidade.
Em discussões de testes de integração, o projeto testado é frequentemente chamado de System Under Test, ou "SUT" para abreviar. "SUT" é usado ao longo deste artigo para se referir ao aplicativo ASP.NET Core que está sendo testado.
Não escreva testes de integração para cada de permutação de dados e acesso a arquivos com bancos de dados e sistemas de arquivos. Independentemente de quantos lugares em um aplicativo interagem com bancos de dados e sistemas de arquivos, um conjunto focado de testes de integração de leitura, gravação, atualização e exclusão geralmente é capaz de testar adequadamente os componentes do banco de dados e do sistema de arquivos. Use testes de unidade para testes de rotina de lógica de método que interagem com esses componentes. Em testes de unidade, o uso de falsificações ou simulações de infraestrutura resulta em execução de teste mais rápida.
ASP.NET Testes de integração principais
Os testes de integração no ASP.NET Core requerem o seguinte:
- Um projeto de teste é usado para conter e executar os testes. O projeto de teste tem uma referência ao SUT.
- O projeto de teste cria um host de teste para o SUT e usa um cliente de servidor de teste para lidar com solicitações e respostas com o SUT.
- Um corredor de teste é usado para executar os testes e relatar os resultados do teste.
Os testes de integração seguem uma sequência de eventos que inclui as etapas habituais de teste Preparação, Execuçãoe Verificação:
- O host da SUT está configurado.
- Um cliente de servidor de teste é criado para enviar solicitações ao aplicativo.
- A etapa de teste Organizar é executada: o aplicativo de teste prepara uma solicitação.
- A etapa de teste do Act é executada: o cliente envia a solicitação e recebe a resposta.
- A etapa de teste Assert é executada: A resposta real é validada como aprovação ou falha com base em uma resposta esperada.
- O processo continua até que todos os testes sejam executados.
- Os resultados dos testes são apresentados.
Normalmente, o host da Web de teste é configurado de forma diferente do host normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usados para os testes.
Os componentes de infraestrutura, como o host da Web de teste e o servidor de teste na memória (TestServer), são fornecidos ou gerenciados pelo pacote Microsoft.AspNetCore.Mvc.Testing . O uso deste pacote simplifica a criação e execução de testes.
O pacote Microsoft.AspNetCore.Mvc.Testing
lida com as seguintes tarefas:
- Copia o arquivo de dependências (
.deps
) do SUT para o diretóriobin
do projeto de teste. - Define a raiz de conteúdo para a raiz do projeto SUT para que arquivos estáticos e páginas/visualizações sejam encontrados quando os testes forem executados.
- Fornece a classe WebApplicationFactory para agilizar a inicialização do SUT com
TestServer
.
A documentação dos testes de unidade descreve como configurar um projeto de teste e um executor de testes, juntamente com instruções detalhadas sobre como executar os testes e recomendações sobre como nomear testes e classes de teste.
Separe os testes de unidade dos testes de integração em diferentes projetos. Separar os testes:
- Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
- Permite controlar qual conjunto de testes é executado.
Não há praticamente nenhuma diferença entre a configuração para testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença está na forma como os testes são nomeados. Em uma aplicação Razor Pages, os testes dos endpoints de página geralmente são nomeados com base na classe de modelo de página (por exemplo, IndexPageTests
para testar a integração de componentes na página Índice). Em um aplicativo MVC, os testes geralmente são organizados por classes de controlador e nomeados após os controladores que eles testam (por exemplo, HomeControllerTests
para testar a integração de componentes para o controlador Home).
Pré-requisitos do aplicativo de teste
O projeto de ensaio deve:
- Faça referência ao pacote
Microsoft.AspNetCore.Mvc.Testing
. - Especifique o SDK da Web no arquivo de projeto (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Esses pré-requisitos podem ser vistos no aplicativo de exemplo . Inspecione o arquivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. O aplicativo de exemplo usa a estrutura de teste xUnit e a biblioteca de análise AngleSharp , o que significa que o aplicativo de exemplo também faz referência a:
Em aplicativos que usam xunit.runner.visualstudio
versão 2.4.2 ou posterior, o projeto de teste deve fazer referência ao pacote Microsoft.NET.Test.Sdk
.
O Entity Framework Core também é usado nos testes. Consulte o arquivo de projeto no GitHub.
Ambiente do SUT
Se o ambiente de do SUT não estiver definido, o ambiente padrão será Desenvolvimento.
Testes básicos com o padrão WebApplicationFactory
Exponha a classe Program
implicitamente definida para o projeto de teste seguindo um destes procedimentos:
Exponha tipos internos da aplicação web ao projeto de teste. Isso pode ser feito no arquivo do projeto SUT (
.csproj
):<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Torne a classe
Program
pública usando uma declaração parcial de classe:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
O aplicativo de exemplo usa a
Program
abordagem de classe parcial.
WebApplicationFactory<TEntryPoint> é usado para criar um TestServer para os testes de integração.
TEntryPoint
é a classe de ponto de entrada do SUT, geralmente Program.cs
.
As classes de teste implementam uma interface de fixture de classe (IClassFixture
) para indicar que a classe contém testes e fornecer instâncias de objeto compartilhado entre os testes na classe.
A seguinte classe de teste, BasicTests
, utiliza o WebApplicationFactory
para inicializar o SUT e fornecer um HttpClient a um método de teste, Get_EndpointsReturnSuccessAndCorrectContentType
. O método verifica se o código de status da resposta foi bem-sucedido (200-299) e o cabeçalho Content-Type
está text/html; charset=utf-8
para várias páginas do aplicativo.
CreateClient() cria uma instância de HttpClient
que segue automaticamente redirecionamentos e lida com cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
Por padrão, os cookies não essenciais não são preservados nas solicitações quando a política de consentimento do Regulamento Geral de Proteção de Dados está ativada. Para preservar cookies não essenciais, como os usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Cookies essenciais.
AngleSharp vs Application Parts
para verificações antifalsificação
Este artigo usa o analisador AngleSharp para lidar com as verificações antifalsificação carregando páginas e analisando o HTML. Para testar os endereços de destino das visualizações do controlador e do Razor Pages a um nível mais baixo, sem se preocupar com a forma como são apresentados no navegador, considere a utilização de Application Parts
. A abordagem Application Parts injeta um controlador ou Razor Page no aplicativo que pode ser usado para fazer solicitações JSON para obter os valores necessários. Para obter mais informações, consulte o blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts e de repositório GitHub associado pelo Martin Costello.
Personalizar WebApplicationFactory
A configuração do host da Web pode ser criada independentemente das classes de teste, herdando de WebApplicationFactory<TEntryPoint> para criar uma ou mais fábricas personalizadas:
Herdar de
WebApplicationFactory
e substituir ConfigureWebHost. O IWebHostBuilder permite a configuração da coleção de serviços comIWebHostBuilder.ConfigureServices
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
A propagação do banco de dados no aplicativo de exemplo é executada pelo método
InitializeDbForTests
. O método é descrito na seção Integration tests sample: Test app organization seção.O contexto da base de dados da SUT está registado em
Program.cs
. A execução do "callback"builder.ConfigureServices
da aplicação de teste ocorre depois de o códigoProgram.cs
da aplicação ser executado. Para usar um banco de dados diferente para os testes em relação ao banco de dados da aplicação, o contexto do banco de dados da aplicação deve ser substituído embuilder.ConfigureServices
.O aplicativo de exemplo localiza o descritor de serviço para o contexto do banco de dados e usa o descritor para remover o registro de serviço. Em seguida, a fábrica adiciona um novo
ApplicationDbContext
que usa um banco de dados na memória para os testes.Para se conectar a um banco de dados diferente, altere o
DbConnection
. Para usar um banco de dados de teste do SQL Server:
- Faça referência ao pacote NuGet
Microsoft.EntityFrameworkCore.SqlServer
no arquivo de projeto. - Ligue para
UseInMemoryDatabase
.
Use o
CustomWebApplicationFactory
personalizado em classes de teste. O exemplo a seguir usa a fábrica na classeIndexPageTests
:public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); }
O cliente do aplicativo de exemplo está configurado para impedir que o
HttpClient
siga redirecionamentos. Conforme explicado mais adiante na seção de autenticação simulada, isso permite que os testes verifiquem o resultado da primeira resposta do aplicativo. A primeira resposta é um redirecionamento em muitos desses testes com um cabeçalhoLocation
.Um teste típico usa os métodos
HttpClient
e auxiliar para processar a solicitação e a resposta:[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
Qualquer solicitação POST ao SUT deve satisfazer a verificação antifalsificação realizada automaticamente pelo sistema de antifalsificação de proteção de dados do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:
- Faça um pedido para a página.
- Analise o cookie antifalsificação e solicite o token de validação da resposta.
- Faça a solicitação POST com o token antifalsificação cookie e solicite o token de validação válido.
Os métodos auxiliares de extensão SendAsync
(Helpers/HttpClientExtensions.cs
) e o método auxiliar GetDocumentAsync
(Helpers/HtmlHelpers.cs
) no aplicativo exemplo usam o analisador AngleSharp para lidar com a verificação antifalsificação com os seguintes métodos:
-
GetDocumentAsync
: Recebe o HttpResponseMessage e devolve umIHtmlDocument
.GetDocumentAsync
usa uma fábrica que prepara uma resposta virtual com base noHttpResponseMessage
original. Para obter mais informações, consulte a documentação do AngleSharp . -
SendAsync
métodos de extensão para oHttpClient
compõem um HttpRequestMessage e chamam SendAsync(HttpRequestMessage) para enviar solicitações ao SUT. As sobrecargas paraSendAsync
aceitam o formulário HTML (IHtmlFormElement
) e o seguinte:- Botão Enviar do formulário (
IHtmlElement
) - Coleção de valores de formulário (
IEnumerable<KeyValuePair<string, string>>
) - Botão Enviar (
IHtmlElement
) e valores de formulário (IEnumerable<KeyValuePair<string, string>>
)
- Botão Enviar do formulário (
AngleSharp é uma biblioteca de análise de terceiros usada para fins de demonstração neste artigo e na aplicação de exemplo. O AngleSharp não é suportado ou necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o Html Agility Pack (HAP). Outra abordagem é escrever código para lidar diretamente com o token de verificação de solicitações do sistema antifalsificação e o cookie de antifalsificação do sistema. Consulte AngleSharp vs Application Parts
para verificações antifalsificação neste artigo para obter mais informações.
O EF-Core provedor de banco de dados na memória pode ser usado para testes limitados e básicos, no entanto, o provedor SQLite é a escolha recomendada para testes na memória.
Consulte Estender inicialização com filtros de inicialização, que mostra como configurar o middleware usando IStartupFilter, o que é útil quando um teste requer um serviço personalizado ou middleware.
Personalize o cliente com WithWebHostBuilder
Quando uma configuração adicional é necessária dentro de um método de teste, WithWebHostBuilder cria um novo WebApplicationFactory
com um IWebHostBuilder que é ainda mais personalizado pela configuração.
O código de exemplo faz uma chamada ao WithWebHostBuilder
para substituir serviços configurados por stubs de teste. Para obter mais informações e exemplos de uso, consulte de serviços simulados de injeção neste artigo.
O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot
do aplicativo de exemplo demonstra o uso de WithWebHostBuilder
. Este teste executa uma exclusão de registro no banco de dados acionando um envio de formulário no SUT.
Como outro teste na classe IndexPageTests
executa uma operação que exclui todos os registros no banco de dados e pode ser executado antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot
, o banco de dados é repropagado nesse método de teste para garantir que um registro esteja presente para o SUT excluir. A seleção do primeiro botão de exclusão do formulário messages
no SUT é simulada na solicitação ao SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Opções do cliente
Consulte a página WebApplicationFactoryClientOptions para obter os padrões e as opções disponíveis ao criar instâncias HttpClient
.
Crie a classe WebApplicationFactoryClientOptions
e passe-a para o método CreateClient():
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
NOTA: Para evitar avisos de redirecionamento HTTPS em logs ao usar o middleware de redirecionamento HTTPS, defina BaseAddress = new Uri("https://localhost")
Injetar serviços simulados
Os serviços podem ser substituídos em um teste com uma chamada para ConfigureTestServices no construtor de hosts. Para definir o escopo dos serviços substituídos para o teste em si, o método WithWebHostBuilder é usado para recuperar um construtor de host. Isso pode ser visto nos seguintes testes:
- Get_QuoteService_ProvidesQuoteInPage
- Obtenha_PaginaDePerfilDoGithubPodeObterUmUsuarioGithub
- Obter_PáginaSeguraRetornadaParaUmUtilizadorAutenticado
O SUT de exemplo inclui um serviço delimitado que retorna uma citação. A citação é incorporada num campo oculto na página Índice quando esta é solicitada.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
A seguinte marcação é gerada quando o aplicativo SUT é executado:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Para testar o serviço e a injeção de cotação em um teste de integração, um serviço simulado é injetado no SUT pelo teste. O serviço simulado substitui o QuoteService
do aplicativo por um serviço fornecido pelo aplicativo de teste, chamado TestQuoteService
:
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
é chamado e um serviço com um escopo é registado:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
A marcação produzida durante a execução do teste reflete o texto da citação fornecido por TestQuoteService
, assim a afirmação confirma-se.
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Autenticação simulada
Os testes na classe AuthTests
verificam um ponto de extremidade seguro:
- Redireciona um usuário não autenticado para a página de login do aplicativo.
- Retorna conteúdo para um usuário autenticado.
No SUT, a página /SecurePage
usa uma convenção AuthorizePage para aplicar um AuthorizeFilter à página. Para mais informações, consulte as convenções de autorização de páginas Razor.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
No teste de Get_SecurePageRedirectsAnUnauthenticatedUser
, um WebApplicationFactoryClientOptions é definido para não permitir redirecionamentos definindo AllowAutoRedirect como false
:
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
Ao não permitir que o cliente siga o redirecionamento, as seguintes verificações podem ser feitas:
- O código de status retornado pelo SUT pode ser verificado em relação ao resultado HttpStatusCode.Redirect esperado, não o código de status final após o redirecionamento para a página de login, que seria HttpStatusCode.OK.
- O valor do cabeçalho
Location
nos cabeçalhos de resposta é verificado para confirmar que começa comhttp://localhost/Identity/Account/Login
, não com a resposta final da página de início de sessão, onde o cabeçalhoLocation
não estaria presente.
O aplicativo de teste pode simular um AuthenticationHandler<TOptions> em ConfigureTestServices para testar aspetos de autenticação e autorização. Um cenário mínimo devolve um AuthenticateResult.Success:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
O TestAuthHandler
é chamado para autenticar um usuário quando o esquema de autenticação é definido como TestScheme
onde AddAuthentication
está registrado para ConfigureTestServices
. É importante que o esquema de TestScheme
corresponda ao esquema esperado pelo seu aplicativo. Caso contrário, a autenticação não funcionará.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Para obter mais informações sobre WebApplicationFactoryClientOptions
, consulte a seção de opções de cliente.
Testes básicos para middleware de autenticação
Consulte este repositório GitHub para testes básicos de middleware de autenticação. Ele contém um de servidor de teste específico para o cenário de teste.
Definir o ambiente
Defina o ambiente na fábrica de aplicações personalizadas.
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
Como a infraestrutura de teste infere o caminho raiz do conteúdo do aplicativo
O construtor WebApplicationFactory
infere o aplicativo caminho de raiz de conteúdo procurando por um WebApplicationFactoryContentRootAttribute no assembly contendo os testes de integração com uma chave igual ao assembly TEntryPoint
System.Reflection.Assembly.FullName
. Caso um atributo com a chave correta não seja encontrado, WebApplicationFactory
volta à procura de um arquivo de solução (.sln) e acrescenta o nome do assembly TEntryPoint
ao diretório da solução. O diretório raiz do aplicativo (o caminho da raiz do conteúdo) é usado para descobrir exibições e arquivos de conteúdo.
Desativar cópia de sombra
A cópia de sombra faz com que os testes sejam executados em um diretório diferente do diretório de saída. Se os testes dependerem do carregamento de ficheiros relativos ao Assembly.Location
e surgirem problemas, talvez seja necessário desativar a cópia de sombra.
Para desabilitar a cópia de sombra ao usar xUnit, crie um arquivo de xunit.runner.json
no diretório do projeto de teste, com a definição de configuração correta:
{
"shadowCopy": false
}
Eliminação de objetos
Depois que os testes da implementação do IClassFixture
são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory
. Se os objetos instanciados pelo desenvolvedor precisarem ser eliminados, elimine-os na implementação IClassFixture
. Para obter mais informações, consulte Implementing a Dispose method.
Exemplo de testes de integração
A aplicação de exemplo é composta por duas aplicações:.
Aplicação | Diretório de projetos | Descrição |
---|---|---|
Aplicativo de mensagem (o SUT) | src/RazorPagesProject |
Permite que um usuário adicione, exclua um, exclua todos e analise mensagens. |
Aplicativo de teste | tests/RazorPagesProject.Tests |
Usado para testar a integração do SUT. |
Os testes podem ser executados usando os recursos de teste internos de um IDE, como Visual Studio. Se estiver usando Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando no diretório tests/RazorPagesProject.Tests
:
dotnet test
Organização do aplicativo de mensagem (SUT)
O SUT é um sistema de mensagens Razor Pages com as seguintes características:
- A página Índice do aplicativo (
Pages/Index.cshtml
ePages/Index.cshtml.cs
) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (média de palavras por mensagem). - Uma mensagem é descrita pela classe
Message
(Data/Message.cs
) com duas propriedades:Id
(chave) eText
(mensagem). A propriedadeText
é obrigatória e limitada a 200 caracteres. - As mensagens são armazenadas usando a base de dados na memória do Entity Framework †.
- O aplicativo contém uma camada de acesso a dados (DAL) em sua classe de contexto de banco de dados,
AppDbContext
(Data/AppDbContext.cs
). - Se o banco de dados estiver vazio na inicialização do aplicativo, o armazenamento de mensagens será inicializado com três mensagens.
- O aplicativo inclui um
/SecurePage
que só pode ser acessado por um usuário autenticado.
†O artigo do EF, Test with InMemory, explica como usar um banco de dados na memória para testes com o MSTest. Este tópico usa o xUnit estrutura de teste. Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.
Embora o aplicativo não use o padrão de repositório e não seja um exemplo eficaz do padrão de Unidade de Trabalho (UoW), o Razor Pages oferece suporte a esses padrões de desenvolvimento. Para mais informações, consulte Projetar a camada de persistência da infraestrutura e testar a lógica do controlador (o exemplo implementa o padrão de repositório).
Testar a organização do aplicativo
O aplicativo de teste é um aplicativo de console dentro do diretório tests/RazorPagesProject.Tests
.
Diretório de aplicativos de teste | Descrição |
---|---|
AuthTests |
Contém métodos de teste para:
|
BasicTests |
Contém um método de teste para roteamento e tipo de conteúdo. |
IntegrationTests |
Contém os testes de integração para a página Índice usando a classe WebApplicationFactory personalizada. |
Helpers/Utilities |
|
A estrutura de teste é xUnit. Os testes de integração são realizados usando o Microsoft.AspNetCore.TestHost, que inclui o TestServer. Como o pacote Microsoft.AspNetCore.Mvc.Testing
é usado para configurar o host de teste e o servidor de teste, os pacotes TestHost
e TestServer
não exigem referências diretas de pacote no arquivo de projeto do aplicativo de teste ou na configuração do desenvolvedor no aplicativo de teste.
Os testes de integração geralmente exigem um pequeno conjunto de dados no banco de dados antes da execução do teste. Por exemplo, um teste de exclusão chama para uma exclusão de registro de banco de dados, portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão seja bem-sucedida.
A aplicação de exemplo preenche a base de dados com três mensagens em Utilities.cs
para utilização nos testes quando estes forem executados.
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
O contexto da base de dados da SUT está registado em Program.cs
. O retorno de chamada builder.ConfigureServices
do aplicativo de teste é executado depois de o código Program.cs
do aplicativo é executado. Para usar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices
. Para mais informações, consulte a secção Customize WebApplicationFactory.
Recursos adicionais
Este artigo pressupõe uma compreensão básica de testes de unidade. Se não estiver familiarizado com conceitos de teste, consulte o artigo Teste Unitário no .NET Core e .NET Standard e seu conteúdo vinculado.
Exibir ou baixar o código de exemplo (como baixar)
A aplicação de exemplo é uma aplicação Razor Páginas e pressupõe um entendimento básico de Razor Páginas. Se não estiver familiarizado com o Razor Pages, consulte os seguintes artigos:
- Introdução ao Razor Pages
- Comece com Razor Pages
- Testes de unidade de Razor Pages
Para testar SPAs, recomendamos uma ferramenta como Playwright for .NET, que pode automatizar um navegador.
Introdução aos testes de integração
Os testes de integração avaliam os componentes de um aplicativo em um nível mais amplo do que testes de unidade. Os testes de unidade são usados para testar componentes de software isolados, como métodos de classe individuais. Os testes de integração confirmam que dois ou mais componentes do aplicativo trabalham juntos para produzir um resultado esperado, possivelmente incluindo todos os componentes necessários para processar totalmente uma solicitação.
Esses testes mais amplos são usados para testar a infraestrutura e toda a estrutura do aplicativo, geralmente incluindo os seguintes componentes:
- Base de dados
- Sistema de ficheiros
- Dispositivos de rede
- Fluxo de solicitação-resposta
Os testes de unidade usam componentes fabricados, conhecidos como objetos fictícios ou objetos simulados, no lugar de componentes de infraestrutura.
Em contraste com os testes de unidade, os testes de integração:
- Use os componentes reais que o aplicativo usa na produção.
- Exigem mais código e processamento de dados.
- Leve mais tempo para correr.
Portanto, limite o uso de testes de integração aos cenários de infraestrutura mais importantes. Se um comportamento puder ser testado usando um teste de unidade ou um teste de integração, escolha o teste de unidade.
Em discussões de testes de integração, o projeto testado é frequentemente chamado de System Under Test, ou "SUT" para abreviar. "SUT" é usado ao longo deste artigo para se referir ao aplicativo ASP.NET Core que está sendo testado.
Não escreva testes de integração para cada de permutação de dados e acesso a arquivos com bancos de dados e sistemas de arquivos. Independentemente de quantos lugares em um aplicativo interagem com bancos de dados e sistemas de arquivos, um conjunto focado de testes de integração de leitura, gravação, atualização e exclusão geralmente é capaz de testar adequadamente os componentes do banco de dados e do sistema de arquivos. Use testes de unidade para testes de rotina de lógica de método que interagem com esses componentes. Em testes de unidade, o uso de falsificações ou simulações de infraestrutura resulta em execução de teste mais rápida.
ASP.NET Testes de integração principais
Os testes de integração no ASP.NET Core requerem o seguinte:
- Um projeto de teste é usado para conter e executar os testes. O projeto de teste tem uma referência ao SUT.
- O projeto de teste cria um host de teste para o SUT e usa um cliente de servidor de teste para lidar com solicitações e respostas com o SUT.
- Um corredor de teste é usado para executar os testes e relatar os resultados do teste.
Os testes de integração seguem uma sequência de eventos que incluem as etapas habituais de teste Preparar, Atuare Validar:
- O servidor web da SUT está configurado.
- Um cliente de servidor de teste é criado para enviar solicitações ao aplicativo.
- A etapa de teste Arrange é executada: A aplicação de teste prepara uma solicitação.
- A etapa de teste do Act é executada: o cliente envia a solicitação e recebe a resposta.
- A etapa de teste Assert é executada: A resposta real é validada como aprovada ou reprovada baseada em uma resposta esperada.
- O processo continua até que todos os testes sejam executados.
- Os resultados dos testes foram divulgados.
Normalmente, o host da Web de teste é configurado de forma diferente do host normal do aplicativo para as execuções de teste. Por exemplo, um banco de dados diferente ou configurações de aplicativo diferentes podem ser usados para os testes.
Os componentes de infraestrutura, como o host da Web de teste e o servidor de teste na memória (TestServer), são fornecidos ou gerenciados pelo pacote de Microsoft.AspNetCore.Mvc.Testing. O uso deste pacote simplifica a criação e execução de testes.
O pacote Microsoft.AspNetCore.Mvc.Testing
lida com as seguintes tarefas:
- Copia o arquivo de dependências (
.deps
) do SUT para o diretóriobin
do projeto de teste. - Define a raiz de conteúdo para a raiz do projeto SUT para que arquivos estáticos e páginas/visualizações sejam encontrados quando os testes forem executados.
- Fornece a classe WebApplicationFactory para simplificar a inicialização do SUT com
TestServer
.
A documentação dos testes de unidade descreve como configurar um projeto de teste e uma ferramenta de execução de testes, juntamente com instruções detalhadas sobre como executar testes e recomendações sobre como nomear testes e classes de teste.
Separe os testes de unidade dos testes de integração em diferentes projetos. Separar os testes:
- Ajuda a garantir que os componentes de teste de infraestrutura não sejam incluídos acidentalmente nos testes de unidade.
- Permite controlar qual conjunto de testes é executado.
Não há praticamente nenhuma diferença entre a configuração para testes de aplicativos do Razor Pages e aplicativos MVC. A única diferença está na forma como os testes são nomeados. Em uma aplicação Razor Pages, os testes dos pontos de extremidade das páginas geralmente são nomeados conforme a classe do modelo de página (por exemplo, IndexPageTests
para testar a integração de componentes para a página Índice). Em um aplicativo MVC, os testes geralmente são organizados por classes de controlador e nomeados após os controladores que eles testam (por exemplo, HomeControllerTests
para testar a integração de componentes para o controlador Home).
Pré-requisitos do aplicativo de teste
O projeto de ensaio deve:
- Faça referência ao pacote
Microsoft.AspNetCore.Mvc.Testing
. - Especifique o SDK da Web no arquivo de projeto (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Esses pré-requisitos podem ser vistos no aplicativo de exemplo . Inspecione o arquivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
. O aplicativo de exemplo usa a estrutura de teste xUnit e a biblioteca de analisador AngleSharp, portanto, o aplicativo de exemplo também faz referência a:
Em aplicativos que usam xunit.runner.visualstudio
versão 2.4.2 ou posterior, o projeto de teste deve fazer referência ao pacote Microsoft.NET.Test.Sdk
.
O Entity Framework Core também é usado nos testes. Consulte o arquivo de projeto no GitHub.
Ambiente SUT
Se a de ambiente do SUT não estiver definida, o padrão do ambiente será Desenvolvimento.
Testes básicos com o padrão WebApplicationFactory
Exponha a classe Program
implicitamente definida para o projeto de teste seguindo um destes procedimentos:
Exponha os tipos internos da aplicação web para o projeto de teste. Isso pode ser feito no arquivo do projeto SUT (
.csproj
):<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Torne a classe
Program
pública usando uma declaração parcial de classe:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
O aplicativo de exemplo usa a
Program
abordagem de classe parcial.
WebApplicationFactory<TEntryPoint> é usado para criar um TestServer para os testes de integração.
TEntryPoint
é a classe de ponto de entrada do SUT, geralmente Program.cs
.
As classes de teste implementam uma interface de fixação de classe , representada por (IClassFixture
), para indicar que a classe contém testes e para fornecer instâncias de objeto partilhadas para os testes dentro da classe.
A seguinte classe de teste, BasicTests
, usa o WebApplicationFactory
para inicializar o SUT e fornecer uma HttpClient para um método de teste, Get_EndpointsReturnSuccessAndCorrectContentType
. O método verifica se o código de status da resposta é bem-sucedido (200-299) e se o cabeçalho Content-Type
está text/html; charset=utf-8
em várias páginas da aplicação.
CreateClient() cria uma instância de HttpClient
que segue automaticamente redirecionamentos e lida com cookies.
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
Por padrão, os cookies não essenciais não são preservados nas solicitações quando a política de consentimento do Regulamento Geral de Proteção de Dados está ativada. Para preservar cookies não essenciais, como os usados pelo provedor TempData, marque-os como essenciais em seus testes. Para obter instruções sobre como marcar um cookie como essencial, consulte Cookies essenciais.
AngleSharp vs Application Parts
para verificações antifalsificação
Este artigo usa o analisador AngleSharp para lidar com as verificações antifalsificação carregando páginas e analisando o HTML. Para testar os pontos de extremidade das visualizações do controlador e do Razor Pages em um nível inferior, sem se preocupar com a forma como eles são renderizados no navegador, considere usar Application Parts
. A abordagem Application Parts injeta um controlador ou Razor Page no aplicativo que pode ser usado para fazer solicitações JSON para obter os valores necessários. Para obter mais informações, consulte o blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts e e repositório GitHub associado por Martin Costello.
Personalizar WebApplicationFactory
A configuração do alojamento Web pode ser criada de forma independente das classes de teste, herdando de WebApplicationFactory<TEntryPoint>, para configurar uma ou mais fábricas personalizadas.
Herdar de
WebApplicationFactory
e substituir ConfigureWebHost. O IWebHostBuilder permite configurar a coleção de serviços com oIWebHostBuilder.ConfigureServices
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { var dbContextDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbContextOptions<ApplicationDbContext>)); services.Remove(dbContextDescriptor); var dbConnectionDescriptor = services.SingleOrDefault( d => d.ServiceType == typeof(DbConnection)); services.Remove(dbConnectionDescriptor); // Create open SqliteConnection so EF won't automatically close it. services.AddSingleton<DbConnection>(container => { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); return connection; }); services.AddDbContext<ApplicationDbContext>((container, options) => { var connection = container.GetRequiredService<DbConnection>(); options.UseSqlite(connection); }); }); builder.UseEnvironment("Development"); } }
A propagação do banco de dados no aplicativo de exemplo é executada pelo método
InitializeDbForTests
. O método é descrito no exemplo de testes de integração: organização do aplicativo de teste, seção .O contexto da base de dados da SUT está registado em
Program.cs
. O retorno de chamadabuilder.ConfigureServices
do aplicativo de teste é executado depois de o códigoProgram.cs
do aplicativo é executado. Para utilizar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído embuilder.ConfigureServices
.O aplicativo de exemplo localiza o descritor de serviço para o contexto do banco de dados e usa o descritor para remover o registro de serviço. Em seguida, a fábrica adiciona um novo
ApplicationDbContext
que usa um banco de dados na memória para os testes.Para se conectar a um banco de dados diferente, altere o
DbConnection
. Para usar um banco de dados de teste do SQL Server:- Faça referência ao pacote NuGet
Microsoft.EntityFrameworkCore.SqlServer
no arquivo de projeto. - Ligue para
UseInMemoryDatabase
.
- Faça referência ao pacote NuGet
Utilize o
CustomWebApplicationFactory
personalizado em classes de teste. O exemplo a seguir usa a fábrica na classeIndexPageTests
:public class IndexPageTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly HttpClient _client; private readonly CustomWebApplicationFactory<Program> _factory; public IndexPageTests( CustomWebApplicationFactory<Program> factory) { _factory = factory; _client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); } }
O cliente do aplicativo de exemplo está configurado para impedir que o
HttpClient
siga redirecionamentos. Conforme explicado mais adiante na seção de autenticação simulada, isso permite que os testes verifiquem o resultado da primeira resposta do aplicativo. A primeira resposta é um redirecionamento em muitos desses testes com um cabeçalhoLocation
.Um teste típico usa os métodos
HttpClient
e auxiliar para processar a solicitação e a resposta:[Fact] public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot() { // Arrange var defaultPage = await _client.GetAsync("/"); var content = await HtmlHelpers.GetDocumentAsync(defaultPage); //Act var response = await _client.SendAsync( (IHtmlFormElement)content.QuerySelector("form[id='messages']"), (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']")); // Assert Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("/", response.Headers.Location.OriginalString); }
Qualquer solicitação POST ao SUT deve satisfazer a verificação antifalsificação feita automaticamente pelo sistema antifalsificação de proteção de dados do aplicativo. Para organizar a solicitação POST de um teste, o aplicativo de teste deve:
- Faça um pedido para a página.
- Interprete o elemento antifalsificação cookie e solicite um token de validação da resposta.
- Faça a solicitação POST com o token antifalsificação cookie e o token de validação necessário incluído.
Os métodos de extensão auxiliar SendAsync
(Helpers/HttpClientExtensions.cs
) e o método auxiliar GetDocumentAsync
(Helpers/HtmlHelpers.cs
) na aplicação de exemplo usam o analisador AngleSharp para manipular a verificação antifalsificação com os seguintes métodos:
-
GetDocumentAsync
: Recebe o HttpResponseMessage e devolve umIHtmlDocument
.GetDocumentAsync
usa uma fábrica que prepara uma resposta virtual com base noHttpResponseMessage
original. Para mais informações, consulte a documentação do AngleSharp . -
SendAsync
métodos de extensão para oHttpClient
compõem um HttpRequestMessage e chamam SendAsync(HttpRequestMessage) para enviar solicitações ao SUT. Sobrecargas deSendAsync
aceitam o formulário HTML (IHtmlFormElement
) e o seguinte:- Botão Enviar do formulário (
IHtmlElement
) - Coleção de valores de formulário (
IEnumerable<KeyValuePair<string, string>>
) - Botão Enviar (
IHtmlElement
) e valores de formulário (IEnumerable<KeyValuePair<string, string>>
)
- Botão Enviar do formulário (
AngleSharp é uma biblioteca de análise de terceiros usada para fins de demonstração neste artigo e no aplicativo de exemplo. O AngleSharp não é suportado ou necessário para testes de integração de aplicativos ASP.NET Core. Outros analisadores podem ser usados, como o Html Agility Pack (HAP). Outra abordagem é escrever código para lidar com o token de verificação de solicitação do sistema antiforgery e o antiforgery cookie diretamente. Consulte AngleSharp vs Application Parts
para verificações antifalsificação neste artigo para obter mais informações.
O EF-Core provedor de banco de dados na memória pode ser usado para testes limitados e básicos, no entanto, o provedor SQLite é a escolha recomendada para testes na memória.
Veja Estender a inicialização com filtros de inicialização que mostra como configurar o middleware usando IStartupFilter, o que é útil quando um teste requer um serviço personalizado ou middleware.
Personalize o cliente com WithWebHostBuilder
Quando uma configuração adicional é necessária dentro de um método de teste, WithWebHostBuilder cria um novo WebApplicationFactory
com um IWebHostBuilder que é ainda mais personalizado pela configuração.
O código de exemplo chama WithWebHostBuilder
para substituir serviços configurados por stubs de teste. Para obter mais informações e exemplos de uso, consulte de serviços simulados de injeção neste artigo.
O método de teste Post_DeleteMessageHandler_ReturnsRedirectToRoot
da aplicação de exemplo demonstra o uso de WithWebHostBuilder
. Este teste executa uma exclusão de registro no banco de dados acionando um envio de formulário no SUT.
Como outro teste na classe IndexPageTests
executa uma operação que exclui todos os registros no banco de dados e pode ser executado antes do método Post_DeleteMessageHandler_ReturnsRedirectToRoot
, o banco de dados é repropagado nesse método de teste para garantir que um registro esteja presente para o SUT excluir. A seleção do primeiro botão de exclusão do formulário messages
no SUT é simulada na solicitação ao SUT:
[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
// Arrange
using (var scope = _factory.Services.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<ApplicationDbContext>();
Utilities.ReinitializeDbForTests(db);
}
var defaultPage = await _client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
//Act
var response = await _client.SendAsync(
(IHtmlFormElement)content.QuerySelector("form[id='messages']"),
(IHtmlButtonElement)content.QuerySelector("form[id='messages']")
.QuerySelector("div[class='panel-body']")
.QuerySelector("button"));
// Assert
Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.OriginalString);
}
Opções do cliente
Consulte a página WebApplicationFactoryClientOptions para obter os padrões e as opções disponíveis ao criar instâncias HttpClient
.
Crie a classe WebApplicationFactoryClientOptions
e passe-a para o método CreateClient():
public class IndexPageTests :
IClassFixture<CustomWebApplicationFactory<Program>>
{
private readonly HttpClient _client;
private readonly CustomWebApplicationFactory<Program>
_factory;
public IndexPageTests(
CustomWebApplicationFactory<Program> factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
}
NOTA: Para evitar avisos de redirecionamento HTTPS em logs ao usar o middleware de redirecionamento HTTPS, defina BaseAddress = new Uri("https://localhost")
Injetar serviços simulados
Os serviços podem ser substituídos em um teste com uma chamada para ConfigureTestServices no construtor de hosts. Para definir o escopo dos serviços substituídos para o teste em si, o método WithWebHostBuilder é usado para recuperar um construtor de host. Isso pode ser visto nos seguintes testes:
- Get_QuoteService_ProvidesQuoteInPage
- Obter_PáginaDePerfilGithubConsegueObterUmUtilizadorGithub
- Get_SecurePageIsReturnedForAnAuthenticatedUser
O SUT de exemplo inclui um serviço de alcance específico que retorna uma cotação. A citação é incorporada num campo oculto na página de Índice quando a página de Índice é solicitada.
Services/IQuoteService.cs
:
public interface IQuoteService
{
Task<string> GenerateQuote();
}
Services/QuoteService.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult<string>(
"Come on, Sarah. We've an appointment in London, " +
"and we're already 30,000 years late.");
}
}
Program.cs
:
services.AddScoped<IQuoteService, QuoteService>();
Pages/Index.cshtml.cs
:
public class IndexModel : PageModel
{
private readonly ApplicationDbContext _db;
private readonly IQuoteService _quoteService;
public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
{
_db = db;
_quoteService = quoteService;
}
[BindProperty]
public Message Message { get; set; }
public IList<Message> Messages { get; private set; }
[TempData]
public string MessageAnalysisResult { get; set; }
public string Quote { get; private set; }
public async Task OnGetAsync()
{
Messages = await _db.GetMessagesAsync();
Quote = await _quoteService.GenerateQuote();
}
Pages/Index.cs
:
<input id="quote" type="hidden" value="@Model.Quote">
A seguinte marcação é gerada quando o aplicativo SUT é executado:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Para testar o serviço e a injeção de cotação em um teste de integração, um serviço simulado é injetado no SUT pelo teste. O serviço simulado substitui o QuoteService
do aplicativo por um serviço fornecido pelo aplicativo de teste, chamado TestQuoteService
:
IntegrationTests.IndexPageTests.cs
:
// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
public Task<string> GenerateQuote()
{
return Task.FromResult(
"Something's interfering with time, Mr. Scarman, " +
"and time is my business.");
}
}
ConfigureTestServices
é chamado e o serviço com escopo é registrado:
[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddScoped<IQuoteService, TestQuoteService>();
});
})
.CreateClient();
//Act
var defaultPage = await client.GetAsync("/");
var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
var quoteElement = content.QuerySelector("#quote");
// Assert
Assert.Equal("Something's interfering with time, Mr. Scarman, " +
"and time is my business.", quoteElement.Attributes["value"].Value);
}
A marcação produzida durante a execução do teste reflete o texto da citação fornecido por TestQuoteService
, assim a asserção passa:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Autenticação simulada
Os testes na classe AuthTests
verificam se um ponto de extremidade seguro:
- Redireciona um usuário não autenticado para a página de login do aplicativo.
- Retorna conteúdo para um usuário autenticado.
No SUT, a página /SecurePage
utiliza a convenção AuthorizePage para aplicar o AuthorizeFilter à página. Para obter mais informações, consulte as convenções de autorização das páginas Razor.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
No teste de Get_SecurePageRedirectsAnUnauthenticatedUser
, um WebApplicationFactoryClientOptions é definido para não permitir redirecionamentos definindo AllowAutoRedirect como false
:
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = _factory.CreateClient(
new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
// Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.StartsWith("http://localhost/Identity/Account/Login",
response.Headers.Location.OriginalString);
}
Ao não permitir que o cliente siga o redirecionamento, as seguintes verificações podem ser feitas:
- O código de status retornado pelo SUT pode ser verificado em relação ao resultado HttpStatusCode.Redirect esperado, não o código de status final após o redirecionamento para a página de login, que seria HttpStatusCode.OK.
- O valor do cabeçalho
Location
nos cabeçalhos de resposta é verificado para confirmar que começa comhttp://localhost/Identity/Account/Login
, não com a resposta final da página de início de sessão, onde o cabeçalhoLocation
não estaria presente.
O aplicativo de teste pode simular um AuthenticationHandler<TOptions> em ConfigureTestServices para testar aspetos de autenticação e autorização. Um cenário mínimo retorna um AuthenticateResult.Success:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
O TestAuthHandler
é chamado para autenticar um usuário quando o esquema de autenticação é definido como TestScheme
onde AddAuthentication
está registrado para ConfigureTestServices
. É importante que o esquema de TestScheme
corresponda ao esquema esperado pelo seu aplicativo. Caso contrário, a autenticação não funcionará.
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(scheme: "TestScheme");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Para obter mais informações sobre WebApplicationFactoryClientOptions
, consulte a seção Opções do Cliente .
Testes básicos para middleware de autenticação
Consulte o repositório GitHub para obter testes básicos do middleware de autenticação. Ele contém um servidor de teste que é específico para o cenário de teste.
Definir o ambiente
Configure o ambiente na fábrica de aplicações personalizadas:
public class CustomWebApplicationFactory<TProgram>
: WebApplicationFactory<TProgram> where TProgram : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<ApplicationDbContext>));
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbConnection));
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<ApplicationDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}
}
Como a infraestrutura de teste infere o caminho raiz do conteúdo do aplicativo
O construtor WebApplicationFactory
infere o aplicativo caminho de raiz de conteúdo procurando por um WebApplicationFactoryContentRootAttribute no assembly contendo os testes de integração com uma chave igual ao assembly TEntryPoint
System.Reflection.Assembly.FullName
. Caso um atributo com a chave correta não seja encontrado, WebApplicationFactory
recorre à busca de um ficheiro de solução (.sln) e adiciona o nome do assembly TEntryPoint
ao diretório de solução. O diretório raiz do aplicativo (o caminho da raiz do conteúdo) é usado para descobrir exibições e arquivos de conteúdo.
Desativar cópia de sombra
A cópia de sombra faz com que os testes sejam executados em um diretório diferente do diretório de saída. Se os testes dependerem do carregamento de arquivos relativos ao Assembly.Location
e você encontrar problemas, talvez seja necessário desabilitar a cópia de sombra.
Para desabilitar a cópia de sombra ao usar xUnit, crie um arquivo de xunit.runner.json
no diretório do projeto de teste, com a definição de configuração correta:
{
"shadowCopy": false
}
Eliminação de objetos
Depois que os testes da implementação do IClassFixture
são executados, TestServer e HttpClient são descartados quando xUnit descarta o WebApplicationFactory
. Se os objetos instanciados pelo desenvolvedor precisarem ser eliminados, elimine-os na implementação IClassFixture
. Para obter mais informações, consulte Implementing a Dispose method.
Exemplo de testes de integração
O de aplicativo de exemplo é composto por dois aplicativos:
Aplicação | Diretório de projetos | Descrição |
---|---|---|
Aplicativo de mensagem (o SUT) | src/RazorPagesProject |
Permite que um usuário adicione, exclua um, exclua todos e analise mensagens. |
Aplicativo de teste | tests/RazorPagesProject.Tests |
Usado para testar a integração do SUT. |
Os testes podem ser executados usando os recursos de teste internos de um IDE, como Visual Studio. Se estiver usando Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando no diretório tests/RazorPagesProject.Tests
:
dotnet test
Organização do aplicativo de mensagem (SUT)
O SUT é um sistema de mensagens Razor Pages com as seguintes características:
- A página Índice do aplicativo (
Pages/Index.cshtml
ePages/Index.cshtml.cs
) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (média de palavras por mensagem). - Uma mensagem é descrita pela classe
Message
(Data/Message.cs
) com duas propriedades:Id
(chave) eText
(mensagem). A propriedadeText
é obrigatória e limitada a 200 caracteres. - As mensagens são armazenadas utilizando o serviço de banco de dados na memória do Entity Framework †.
- O aplicativo contém uma camada de acesso a dados (DAL) em sua classe de contexto de banco de dados,
AppDbContext
(Data/AppDbContext.cs
). - Se o banco de dados estiver vazio na inicialização do aplicativo, o armazenamento de mensagens será inicializado com três mensagens.
- O aplicativo inclui um
/SecurePage
que só pode ser acessado por um usuário autenticado.
†O artigo do EF, Test with InMemory, explica como usar um banco de dados na memória para testes com o MSTest. Este tópico utiliza a estrutura de teste xUnit. Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.
Embora o aplicativo não use o padrão de repositório e não seja um exemplo eficaz do padrão de Unidade de Trabalho (UoW), o Razor Pages oferece suporte a esses padrões de desenvolvimento. Para mais informações, consulte Conceber a camada de persistência de infraestrutura e a lógica do controlador de teste (o exemplo implementa o padrão de repositório).
Testar a organização do aplicativo
O aplicativo de teste é um aplicativo de console dentro do diretório tests/RazorPagesProject.Tests
.
Diretório de aplicativos de teste | Descrição |
---|---|
AuthTests |
Contém métodos de teste para:
|
BasicTests |
Contém um método de teste para roteamento e tipo de conteúdo. |
IntegrationTests |
Contém os testes de integração para a página Índice usando a classe WebApplicationFactory personalizada. |
Helpers/Utilities |
|
A estrutura de teste é xUnit. Os testes de integração são realizados usando o Microsoft.AspNetCore.TestHost, que inclui o TestServer. Como o pacote Microsoft.AspNetCore.Mvc.Testing
é usado para configurar o host de teste e o servidor de teste, os pacotes TestHost
e TestServer
não exigem referências diretas de pacote no arquivo de projeto do aplicativo de teste ou na configuração do desenvolvedor no aplicativo de teste.
Os testes de integração geralmente exigem um pequeno conjunto de dados no banco de dados antes da execução do teste. Por exemplo, um teste de exclusão chama para uma exclusão de registro de banco de dados, portanto, o banco de dados deve ter pelo menos um registro para que a solicitação de exclusão seja bem-sucedida.
O aplicativo de exemplo semeia o banco de dados com três mensagens em Utilities.cs
que os testes podem usar quando são executados:
public static void InitializeDbForTests(ApplicationDbContext db)
{
db.Messages.AddRange(GetSeedingMessages());
db.SaveChanges();
}
public static void ReinitializeDbForTests(ApplicationDbContext db)
{
db.Messages.RemoveRange(db.Messages);
InitializeDbForTests(db);
}
public static List<Message> GetSeedingMessages()
{
return new List<Message>()
{
new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
new Message(){ Text = "TEST RECORD: To the rational mind, " +
"nothing is inexplicable; only unexplained." }
};
}
O contexto da base de dados da SUT está registado em Program.cs
. O retorno de chamada builder.ConfigureServices
do aplicativo de teste é executado depois de o código Program.cs
do aplicativo é executado. Para usar um banco de dados diferente para os testes, o contexto do banco de dados do aplicativo deve ser substituído em builder.ConfigureServices
. Para obter mais informações, consulte a seção Personalizar WebApplicationFactory.