Testar middleware do ASP.NET Core

Por Chris Ross

O middleware pode ser testado isoladamente com TestServer. Ele permite:

  • Instancie um pipeline de aplicativo que contém apenas os componentes que você precisa testar.
  • Envie solicitações personalizadas para verificar o comportamento do middleware.

Vantagens:

  • As solicitações são enviadas na memória em vez de serem serializadas pela rede.
  • Isso evita preocupações adicionais, como gerenciamento de porta e certificados HTTPS.
  • Exceções no middleware podem fluir diretamente de volta para o teste de chamada.
  • É possível personalizar estruturas de dados do servidor, como HttpContext, diretamente no teste.

Configurar o TestServer

No projeto de teste, crie um teste:

  • Crie e inicie um host que usa TestServer.

  • Adicione todos os serviços necessários que o middleware usa.

  • Adicione uma referência de pacote ao projeto para o pacote NuGet Microsoft.AspNetCore.TestHost.

  • Configure o pipeline de processamento para usar o middleware para o teste.

    [Fact]
    public async Task MiddlewareTest_ReturnsNotFoundForRequest()
    {
        using var host = await new HostBuilder()
            .ConfigureWebHost(webBuilder =>
            {
                webBuilder
                    .UseTestServer()
                    .ConfigureServices(services =>
                    {
                        services.AddMyServices();
                    })
                    .Configure(app =>
                    {
                        app.UseMiddleware<MyMiddleware>();
                    });
            })
            .StartAsync();
    
        ...
    }
    

Observação

Para obter diretrizes sobre como adicionar pacotes a aplicativos .NET, consulte os artigos em Instalar e gerenciar pacotes no Fluxo de trabalho de consumo de pacotes (documentação do NuGet). Confirme as versões corretas de pacote em NuGet.org.

Enviar solicitações com HttpClient

Envie uma solicitação usando HttpClient:

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var response = await host.GetTestClient().GetAsync("/");

    ...
}

Afirme o resultado. Primeiro, faça uma declaração oposta ao resultado esperado. Uma execução inicial com uma declaração falsa positiva confirma que o teste falha quando o middleware está sendo executado corretamente. Execute o teste e confirme se o teste falha.

No exemplo a seguir, o middleware deve retornar um código de status 404 (Não Encontrado) quando o ponto de extremidade raiz é solicitado. Faça a primeira execução de teste com Assert.NotEqual( ... );, o que deve falhar:

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var response = await host.GetTestClient().GetAsync("/");

    Assert.NotEqual(HttpStatusCode.NotFound, response.StatusCode);
}

Altere a asserção para testar o middleware em condições operacionais normais. O teste final usa Assert.Equal( ... );. Execute o teste novamente para confirmar que ele seja aprovado.

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var response = await host.GetTestClient().GetAsync("/");

    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

Enviar solicitações com HttpContext

Um aplicativo de teste também pode enviar uma solicitação usando SendAsync(Action<HttpContext>, CancellationToken). No exemplo a seguir, várias verificações são feitas quando https://example.com/A/Path/?and=query é processada pelo middleware:

[Fact]
public async Task TestMiddleware_ExpectedResponse()
{
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder =>
        {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services =>
                {
                    services.AddMyServices();
                })
                .Configure(app =>
                {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();

    var server = host.GetTestServer();
    server.BaseAddress = new Uri("https://example.com/A/Path/");

    var context = await server.SendAsync(c =>
    {
        c.Request.Method = HttpMethods.Post;
        c.Request.Path = "/and/file.txt";
        c.Request.QueryString = new QueryString("?and=query");
    });

    Assert.True(context.RequestAborted.CanBeCanceled);
    Assert.Equal(HttpProtocol.Http11, context.Request.Protocol);
    Assert.Equal("POST", context.Request.Method);
    Assert.Equal("https", context.Request.Scheme);
    Assert.Equal("example.com", context.Request.Host.Value);
    Assert.Equal("/A/Path", context.Request.PathBase.Value);
    Assert.Equal("/and/file.txt", context.Request.Path.Value);
    Assert.Equal("?and=query", context.Request.QueryString.Value);
    Assert.NotNull(context.Request.Body);
    Assert.NotNull(context.Request.Headers);
    Assert.NotNull(context.Response.Headers);
    Assert.NotNull(context.Response.Body);
    Assert.Equal(404, context.Response.StatusCode);
    Assert.Null(context.Features.Get<IHttpResponseFeature>().ReasonPhrase);
}

SendAsync permite a configuração direta de um objeto HttpContext em vez de usar as abstrações HttpClient. Use SendAsync para manipular estruturas disponíveis apenas no servidor, como HttpContext.Items ou HttpContext.Features.

Assim como no exemplo anterior que testou para uma resposta 404 – Não Encontrado, marque o oposto para cada instrução Assert no teste anterior. A marcação confirma que o teste falha corretamente quando o middleware está operando normalmente. Depois de confirmar que o teste falso positivo funciona, defina as instruções finais Assert para as condições e valores esperados do teste. Execute-o novamente para confirmar se o teste foi aprovado.

Adicionar rotas de solicitação

Rotas extras podem ser adicionadas pela configuração usando o teste HttpClient:

	[Fact]
	public async Task TestWithEndpoint_ExpectedResponse ()
	{
		using var host = await new HostBuilder()
			.ConfigureWebHost(webBuilder =>
			{
				webBuilder
					.UseTestServer()
					.ConfigureServices(services =>
					{
						services.AddRouting();
					})
					.Configure(app =>
					{
						app.UseRouting();
						app.UseMiddleware<MyMiddleware>();
						app.UseEndpoints(endpoints =>
						{
							endpoints.MapGet("/hello", () =>
								TypedResults.Text("Hello Tests"));
						});
					});
			})
			.StartAsync();

		var client = host.GetTestClient();

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

		Assert.True(response.IsSuccessStatusCode);
		var responseBody = await response.Content.ReadAsStringAsync();
		Assert.Equal("Hello Tests", responseBody);

Rotas extras também podem ser adicionadas com a abordagem server.SendAsync.

Limitações do TestServer

TestServer:

  • Foi criado para replicar comportamentos do servidor para testar o middleware.
  • Não tenta replicar todos os comportamentos HttpClient.
  • Tenta dar ao cliente acesso ao máximo de controle possível sobre o servidor e com o máximo de visibilidade possível do que está acontecendo no servidor. Por exemplo, ele pode gerar exceções normalmente não geradas por HttpClient para comunicar diretamente o estado do servidor.
  • Não define alguns cabeçalhos específicos de transporte por padrão, pois eles geralmente não são relevantes para o middleware. Para obter mais informações, consulte a próxima seção.
  • Ignora a Stream posição passada por StreamContent. HttpClient envia todo o fluxo da posição inicial, mesmo quando o posicionamento está definido. Saiba mais neste tópico do GitHub.

Cabeçalhos Content-Length e Transfer-Encoding

O TestServer não define cabeçalhos de solicitação ou resposta relacionados ao transporte, como Comprimento do Conteúdo ou Codificação de Transferência. Os aplicativos devem evitar dependendo desses cabeçalhos porque o uso deles varia de acordo com o cliente, o cenário e o protocolo. Se Content-Length e Transfer-Encoding forem necessários para testar um cenário específico, eles poderão ser especificados no teste ao redigir o HttpRequestMessage ou HttpContext. Para obter mais informações, confira os seguintes problemas do GitHub: