Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Dica
Esse conteúdo é um trecho do eBook, Arquiteto de Aplicativos Web Modernos com ASP.NET Core e Azure, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.
"Se você não gosta de testar seu produto por unidade, provavelmente seus clientes também não gostarão de testá-lo." _-Anônimo-
O software de qualquer complexidade pode falhar de maneiras inesperadas em resposta às alterações. Portanto, o teste após fazer alterações é necessário para todos, exceto para os aplicativos mais triviais (ou menos críticos). O teste manual é a maneira mais lenta, menos confiável e mais cara de testar o software. Infelizmente, se os aplicativos não forem projetados para serem testáveis, ele poderá ser o único meio de teste disponível. Os aplicativos escritos para seguir os princípios arquitetônicos dispostos no capítulo 4 devem ser em grande parte testáveis por unidade. Aplicativos ASP.NET Core suportam testes de integração automatizada e testes funcionais.
Tipos de testes automatizados
Há muitos tipos de testes automatizados para aplicativos de software. O teste de nível mais simples e mais baixo é o teste de unidade. Em um nível ligeiramente mais alto, há testes de integração e testes funcionais. Outros tipos de testes, como testes de interface do usuário, testes de carga, testes de estresse e testes de fumaça, estão além do escopo deste documento.
Testes de unidade
Um teste de unidade testa uma única parte da lógica do aplicativo. Pode-se descrever ainda mais listando algumas das coisas que não é. Um teste de unidade não testa como seu código funciona com dependências ou infraestrutura– é para isso que servem os testes de integração. Um teste de unidade não testa a estrutura na qual seu código está escrito. Você deve presumir que ele funciona ou, se não o fizer, arquivar um bug e codificar uma solução alternativa. Um teste de unidade é executado completamente na memória e no processo. Ele não se comunica com o sistema de arquivos, a rede ou um banco de dados. Os testes de unidade só devem testar seu código.
Os testes de unidade, em virtude do fato de testarem apenas uma única unidade do código, sem dependências externas, devem ser executados extremamente rápido. Portanto, você deve ser capaz de executar conjuntos de centenas de testes de unidade em alguns segundos. Execute-os com frequência, de preferência, antes de cada push para um repositório de controle do código-fonte compartilhado e, certamente, a cada build automatizado no servidor de build.
Testes de integração
Embora seja uma boa ideia encapsular seu código que interage com a infraestrutura, como bancos de dados e sistemas de arquivos, você ainda terá parte desse código e provavelmente desejará testá-lo. Além disso, você deve verificar se as camadas do código interagem conforme esperado quando as dependências do aplicativo são totalmente resolvidas. Essa funcionalidade é responsabilidade dos testes de integração. Os testes de integração tendem a ser mais lentos e mais difíceis de configurar do que os testes de unidade, pois geralmente dependem de dependências externas e infraestrutura. Portanto, você deve evitar testar itens que podem ser testados com testes de unidade em testes de integração. Se puder testar determinado cenário com um teste de unidade, você deverá testá-lo com um teste de unidade. Se não puder, considere usar um teste de integração.
Os testes de integração geralmente terão procedimentos de configuração e desmontagem mais complexos do que os testes de unidade. Por exemplo, um teste de integração que vai em relação a um banco de dados real precisará de uma maneira de retornar o banco de dados a um estado conhecido antes de cada execução de teste. À medida que novos testes são adicionados e o esquema de banco de dados de produção evolui, esses scripts de teste tendem a aumentar em tamanho e complexidade. Em muitos sistemas grandes, é impraticável executar conjuntos completos de testes de integração em estações de trabalho do desenvolvedor antes de verificar as alterações no controle do código-fonte compartilhado. Nesses casos, os testes de integração podem ser executados em um servidor de build.
Testes funcionais
Os testes de integração são gravados da perspectiva do desenvolvedor para verificar se alguns componentes do sistema funcionam corretamente juntos. Os testes funcionais são escritos da perspectiva do usuário e verificam a precisão do sistema com base em seus requisitos. O trecho a seguir oferece uma analogia útil para pensar em testes funcionais, em comparação com testes de unidade:
"Muitas vezes o desenvolvimento de um sistema é comparado à construção de uma casa. Embora essa analogia não esteja correta, podemos estendê-la para entender a diferença entre testes funcionais e de unidade. O teste de unidade é análogo a um inspetor de construção visitando o canteiro de obras de uma casa. Ele está focado nos vários sistemas internos da casa, na fundação, estrutura, instalações elétricas, encanamento, entre outros. Ele garante (testes) que as partes da casa funcionarão corretamente e com segurança, ou seja, atenderão ao código de construção. Os testes funcionais nesse cenário são análogos ao proprietário que visita esse mesmo canteiro de obras. Ele assume que os sistemas internos se comportarão adequadamente, que o inspetor de construção está executando sua tarefa. O proprietário está focado em como será viver nesta casa. Ele está preocupado com a aparência da casa, se os vários quartos têm um tamanho confortável, se a casa se encaixa nas necessidades da família, se as janelas estão bem posicionadas para captar o sol da manhã. O proprietário está realizando testes funcionais na casa. Ele tem a perspectiva do usuário. O inspetor de construção está realizando inspeções específicas na casa. Ele tem a perspectiva do construtor.
Origem: Testes de Unidade versus Testes Funcionais
Gosto de dizer "Como desenvolvedores, falhamos de duas maneiras: criamos a coisa errada, ou criamos a coisa errada." Testes de unidade garantem que você está criando a coisa certa; os testes funcionais garantem que você esteja criando a coisa certa.
Como os testes funcionais operam no nível do sistema, eles podem exigir algum grau de automação da interface do usuário. Como testes de integração, eles geralmente funcionam com algum tipo de infraestrutura de teste também. Essa atividade os torna mais lentos e mais frágeis do que os testes de unidade e integração. Você deve ter apenas tantos testes funcionais quanto precisar para ter certeza de que o sistema está se comportando como os usuários esperam.
Testando pirâmide
Martin Fowler escreveu sobre a pirâmide de teste, um exemplo do qual é mostrado na Figura 9-1.
Figura 9-1. Testando pirâmide
As diferentes camadas da pirâmide e seus tamanhos relativos representam diferentes tipos de testes e quantos você deve escrever para seu aplicativo. Como você pode ver, a recomendação é ter uma grande base de testes de unidade, com suporte por uma camada menor de testes de integração, com uma camada ainda menor de testes funcionais. O ideal é que cada camada tenha apenas testes que não possam ser executados adequadamente em uma camada inferior. Tenha em mente a pirâmide de teste ao tentar decidir qual tipo de teste você precisa para um cenário específico.
O que testar
Um problema comum para desenvolvedores que são inexperientes em escrever testes automatizados é decidir o que testar. Um bom ponto de partida é testar a lógica condicional. Em qualquer lugar onde você tenha um método cujo comportamento muda de acordo com uma instrução condicional (if-else, switch, etc.), é possível criar pelo menos alguns testes que confirmem o comportamento correto para certas condições. Se o código tiver condições de erro, é bom escrever pelo menos um teste para o "caminho feliz" por meio do código (sem erros) e pelo menos um teste para o "caminho triste" (com erros ou resultados atípicos) para confirmar que seu aplicativo se comporta conforme o esperado diante de erros. Por fim, tente se concentrar em testar coisas que podem falhar, em vez de se concentrar em métricas como cobertura de código. Em geral, mais cobertura de código é melhor do que menos. No entanto, escrever mais alguns testes de um método complexo e comercialmente crítico geralmente é um melhor uso do tempo do que escrever testes para propriedades automáticas apenas para melhorar as métricas de cobertura de código de teste.
Organizando projetos de teste
Os projetos de teste podem ser organizados da maneira que funcionar melhor para você. É uma boa ideia separar testes por tipo (teste de unidade, teste de integração) e pelo que eles estão testando (por projeto, por namespace). Se essa separação consiste em pastas em um único projeto de teste ou em vários projetos de teste, é uma decisão de design. Um projeto é mais simples, mas para projetos grandes com muitos testes ou para executar mais facilmente diferentes conjuntos de testes, talvez você queira ter vários projetos de teste diferentes. Muitas equipes organizam projetos de teste com base no projeto que estão testando, o que para aplicativos com mais de alguns projetos pode resultar em um grande número de projetos de teste, especialmente se você ainda dividi-los de acordo com o tipo de testes em cada projeto. Uma abordagem de meio-termo é ter um projeto por tipo de teste, por aplicativo, com pastas dentro dos projetos de teste para indicar o projeto (e a classe) que está sendo testado.
Uma abordagem comum é organizar os projetos de aplicativo em uma pasta 'src' e os projetos de teste do aplicativo em uma pasta paralela 'tests'. Você pode criar pastas de solução correspondentes no Visual Studio, se achar essa organização útil.
Figura 9-2. Organizar testes na sua solução
Você pode usar qualquer estrutura de teste que preferir. A estrutura xUnit funciona bem e é a base para a qual todos os testes do ASP.NET Core e EF Core são escritos. Você pode adicionar um projeto de teste xUnit no Visual Studio usando o modelo mostrado na Figura 9-3 ou da CLI usando dotnet new xunit
.
Figura 9-3. Adicionar um projeto de teste xUnit no Visual Studio
Nomenclatura de testes
Nomeie seus testes de forma consistente, com nomes que indicam o que cada teste faz. Uma abordagem com a qual tive grande sucesso é nomear classes de teste de acordo com a classe e o método que estão testando. Essa abordagem resulta em muitas classes de teste pequenas, mas deixa extremamente claro pelo que cada teste é responsável. Com o nome da classe de teste configurado, para identificar a classe e o método a serem testados, o nome do método de teste pode ser usado para especificar o comportamento que está sendo testado. Esse nome deve incluir o comportamento esperado e quaisquer entradas ou suposições que devem produzir esse comportamento. Alguns exemplos de nomes de teste:
CatalogControllerGetImage.CallsImageServiceWithId
CatalogControllerGetImage.LogsWarningGivenImageMissingException
CatalogControllerGetImage.ReturnsFileResultWithBytesGivenSuccess
CatalogControllerGetImage.ReturnsNotFoundResultGivenImageMissingException
Uma variação dessa abordagem termina o nome de cada classe de teste com "Should" e modifica ligeiramente os tempos verbais:
CatalogControllerGetImage
Se.
ChamarImageServiceWithId
CatalogControllerGetImage
Se.
ToraWarningGivenImageMissingException
Algumas equipes acham a segunda abordagem de nomenclatura mais clara, embora um pouco mais detalhada. De qualquer forma, tente usar uma convenção de nomenclatura que forneça insights sobre o comportamento do teste, de modo que, quando um ou mais testes falharem, é óbvio em seus nomes quais casos falharam. Evite nomear seus testes vagamente, como ControllerTests.Test1, pois esses nomes não oferecem nenhum valor quando você os vê nos resultados do teste.
Se você seguir uma convenção de nomenclatura como a acima que produz muitas classes de teste pequenas, é uma boa ideia organizar ainda mais seus testes usando pastas e namespaces. A Figura 9-4 mostra uma abordagem para organizar testes por pasta em vários projetos de teste.
Figura 9-4. Organizar classes de teste por pasta com base na classe que está sendo testada.
Se uma classe de aplicativo específica tiver muitos métodos sendo testados (e, portanto, muitas classes de teste), talvez faça sentido colocar essas classes em uma pasta correspondente à classe de aplicativo. Essa organização não é diferente de como você pode organizar arquivos em pastas em outros lugares. Se você tiver mais de três ou quatro arquivos relacionados em uma pasta que contém muitos outros arquivos, geralmente é útil movê-los para sua própria subpasta.
Testes de unidade em aplicativos ASP.NET Core
Em um aplicativo ASP.NET Core bem projetado, a maior parte da complexidade e da lógica de negócios será encapsulada em entidades de negócios e uma variedade de serviços. O aplicativo ASP.NET Core MVC em si, com seus controladores, filtros, modelos de exibição e exibições, deve exigir poucos testes de unidade. Grande parte da funcionalidade de uma determinada ação está fora do próprio método de ação. Testar se o roteamento ou o tratamento de erros global funcionam corretamente não pode ser feito efetivamente com um teste de unidade. Da mesma forma, todos os filtros, incluindo filtros de validação e autenticação e autorização de modelo, não podem ser testados por unidade com um teste direcionado ao método de ação de um controlador. Sem essas fontes de comportamento, a maioria dos métodos de ação deve ser trivialmente pequena, delegando a maior parte de seu trabalho para serviços que podem ser testados independentemente do controlador que os usa.
Às vezes, você precisará refatorar seu código para testá-lo por unidade. Frequentemente, essa atividade envolve identificar abstrações e usar a injeção de dependência para acessar a abstração no código que você deseja testar, em vez de codificar diretamente na infraestrutura. Por exemplo, considere este método de ação fácil para exibir imagens:
[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
var contentRoot = _env.ContentRootPath + "//Pics";
var path = Path.Combine(contentRoot, id + ".png");
Byte[] b = System.IO.File.ReadAllBytes(path);
return File(b, "image/png");
}
Submeter esse método ao teste de unidade é dificultado por sua dependência direta de System.IO.File
, que ele usa para ler o sistema de arquivos. Você pode testar esse comportamento para garantir que ele funcione conforme o esperado, mas fazer isso com arquivos reais é um teste de integração. Vale a pena observar que você não pode testar a rota desse método por unidade. Você verá como fazer esse teste com um teste funcional em breve.
Se você não puder testar o comportamento do sistema de arquivos diretamente e não puder testar a rota, o que há para testar? Bem, depois de refatorar para tornar possíveis os testes unitários, você pode descobrir alguns casos de teste e comportamentos ausentes, como o tratamento de erros. O que o método faz quando um arquivo não é encontrado? O que ela deve fazer? Neste exemplo, o método refatorado tem esta aparência:
[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
byte[] imageBytes;
try
{
imageBytes = _imageService.GetImageBytesById(id);
}
catch (CatalogImageMissingException ex)
{
_logger.LogWarning($"No image found for id: {id}");
return NotFound();
}
return File(imageBytes, "image/png");
}
_logger
e _imageService
são injetados como dependências. Agora você pode testar se a mesma ID passada para o método de ação é passada para _imageService
, e que os bytes resultantes são retornados como parte do FileResult. Você também pode testar se o registro em log de erros está acontecendo conforme o esperado e que um NotFound
resultado será retornado se a imagem estiver ausente, supondo que esse comportamento seja um comportamento importante do aplicativo (ou seja, não apenas o código temporário que o desenvolvedor adicionou para diagnosticar um problema). A lógica de arquivo real foi movida para um serviço de implementação separado e foi aumentada para retornar uma exceção específica do aplicativo para o caso de um arquivo ausente. Você pode testar essa implementação de forma independente usando um teste de integração.
Na maioria dos casos, você desejará usar manipuladores de exceção globais em seus controladores, portanto, a quantidade de lógica neles deve ser mínima e provavelmente não vale a pena testar a unidade. Faça a maioria dos testes de ações do controlador usando testes funcionais e a TestServer
classe descrita abaixo.
Testes de integração de aplicativos ASP.NET Core
A maioria dos testes de integração em seus aplicativos do ASP.NET Core deve estar testando serviços e outros tipos de implementação definidos em seu projeto de infraestrutura. Por exemplo, você pode testar se o EF Core estava atualizando e recuperando com êxito os dados esperados de suas classes de acesso a dados que residem no projeto infraestrutura. A melhor maneira de testar se o projeto do Core MVC do ASP.NET está se comportando corretamente é com testes funcionais executados em seu aplicativo em execução em um host de teste.
Testes funcionais de aplicativos ASP.NET Core
A classe TestServer
torna bastante fácil escrever testes funcionais para aplicativos ASP.NET Core. Você configura um TestServer
usando um WebHostBuilder
ou HostBuilder
diretamente (como faz normalmente com seu aplicativo) ou com o tipo WebApplicationFactory
(disponível desde a versão 2.1). Tente igualar o host de teste ao host de produção o mais próximo possível, para que seus testes exerçam um comportamento semelhante ao que o aplicativo fará na produção. A WebApplicationFactory
classe é útil para configurar o ContentRoot do TestServer, que é usado pelo ASP.NET Core para localizar recursos estáticos como Exibições.
Você pode criar testes funcionais simples criando uma classe de teste que implementa IClassFixture<WebApplicationFactory<TEntryPoint>>
, onde TEntryPoint
está a classe do Startup
aplicativo Web. Com essa interface em vigor, o acessório de teste pode criar um cliente usando o método de fábrica CreateClient
:
public class BasicWebTests : IClassFixture<WebApplicationFactory<Program>>
{
protected readonly HttpClient _client;
public BasicWebTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
// write tests that use _client
}
Dica
Se você estiver usando a configuração mínima de API em seu arquivo de Program.cs , por padrão, a classe será declarada interna e não estará acessível no projeto de teste. Em vez disso, você pode escolher qualquer outra classe de instância em seu projeto Web ou adicioná-la ao arquivo Program.cs :
// Make the implicit Program class public so test projects can access it
public partial class Program { }
Com frequência, você desejará executar algumas configurações adicionais do seu site antes que cada teste seja executado, como configurar o aplicativo para usar um armazenamento de dados na memória e, em seguida, propagar o aplicativo com dados de teste. Para obter essa funcionalidade, crie sua própria subclasse WebApplicationFactory<TEntryPoint>
e substitua seu ConfigureWebHost
método. O exemplo a seguir é do projeto eShopOnWeb FunctionalTests e é usado como parte dos testes no aplicativo Web principal.
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace Microsoft.eShopWeb.FunctionalTests.Web;
public class WebTestFixture : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureServices(services =>
{
services.AddEntityFrameworkInMemoryDatabase();
// Create a new service provider.
var provider = services
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Add a database context (ApplicationDbContext) using an in-memory
// database for testing.
services.AddDbContext<CatalogContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
options.UseInternalServiceProvider(provider);
});
services.AddDbContext<AppIdentityDbContext>(options =>
{
options.UseInMemoryDatabase("Identity");
options.UseInternalServiceProvider(provider);
});
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<CatalogContext>();
var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();
var logger = scopedServices
.GetRequiredService<ILogger<WebTestFixture>>();
// Ensure the database is created.
db.Database.EnsureCreated();
try
{
// Seed the database with test data.
CatalogContextSeed.SeedAsync(db, loggerFactory).Wait();
// seed sample user data
var userManager = scopedServices.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scopedServices.GetRequiredService<RoleManager<IdentityRole>>();
AppIdentityDbContextSeed.SeedAsync(userManager, roleManager).Wait();
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred seeding the " +
"database with test messages. Error: {ex.Message}");
}
}
});
}
}
Os testes podem usar esse WebApplicationFactory personalizado usando-o para criar um cliente e, em seguida, fazer solicitações para o aplicativo usando essa instância do cliente. O aplicativo terá dados semeados que podem ser usados como parte das declarações do teste. O teste a seguir verifica se a home page do aplicativo eShopOnWeb é carregada corretamente e inclui uma listagem de produtos que foi adicionada ao aplicativo como parte dos dados de semente.
using Microsoft.eShopWeb.FunctionalTests.Web;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages;
[Collection("Sequential")]
public class HomePageOnGet : IClassFixture<WebTestFixture>
{
public HomePageOnGet(WebTestFixture factory)
{
Client = factory.CreateClient();
}
public HttpClient Client { get; }
[Fact]
public async Task ReturnsHomePageWithProductListing()
{
// Arrange & Act
var response = await Client.GetAsync("/");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
// Assert
Assert.Contains(".NET Bot Black Sweatshirt", stringResponse);
}
}
Esse teste funcional emprega a pilha completa do aplicativo ASP.NET Core MVC/Razor Pages, incluindo todos os middlewares, filtros e associadores que possam estar em vigor. Ele verifica se uma determinada rota ("/") retorna o código de status de sucesso esperado e a saída HTML. Ele faz isso sem configurar um servidor web real e evita grande parte da fragilidade que o uso de um servidor web real durante testes pode enfrentar (por exemplo, problemas com configurações de firewall). Os testes funcionais executados no TestServer geralmente são mais lentos do que os testes de integração e de unidade, mas são muito mais rápidos do que os testes que seriam executados pela rede para um servidor Web de teste. Use os testes funcionais para garantir que pilha de front-end do aplicativo funcione conforme o esperado. Esses testes são especialmente úteis quando você encontra a duplicação em seus controladores ou páginas e aborda a duplicação adicionando filtros. O ideal é que essa refatoração não altere o comportamento do aplicativo e um conjunto de testes funcionais verificará se esse é o caso.
Referências – Testar ASP.NET principais aplicativos MVC
- Testando no ASP.NET Core
https://learn.microsoft.com/aspnet/core/testing/- Convenção de nomenclatura de teste de unidade
https://ardalis.com/unit-test-naming-convention- Testando o EF Core
https://learn.microsoft.com/ef/core/miscellaneous/testing/- Testes de integração no ASP.NET Core
https://learn.microsoft.com/aspnet/core/test/integration-tests