Testare middleware di ASP.NET Core

Di Chris Ross

Il middleware può essere testato in isolamento con TestServer. Consente di:

  • Creare un'istanza di una pipeline di app contenente solo i componenti necessari per il test.
  • Inviare richieste personalizzate per verificare il comportamento del middleware.

Vantaggi:

  • Le richieste vengono inviate in memoria anziché serializzate in rete.
  • In questo modo si evitano problemi aggiuntivi, ad esempio la gestione delle porte e i certificati HTTPS.
  • Le eccezioni nel middleware possono tornare direttamente al test chiamante.
  • È possibile personalizzare le strutture dei dati del server, ad esempio HttpContext, direttamente nel test.

Configurare TestServer

Nel progetto di test creare un test:

  • Compilare e avviare un host che usa TestServer.

  • Aggiungere tutti i servizi necessari usati dal middleware.

  • Aggiungere un riferimento al pacchetto al progetto per il Microsoft.AspNetCore.TestHost pacchetto NuGet.

  • Configurare la pipeline di elaborazione per usare il middleware per il test.

    [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();
    
        ...
    }
    

Nota

Per indicazioni sull'aggiunta di pacchetti alle app .NET, vedere gli articoli sotto Installare e gestire pacchetti in Flusso di lavoro dell'utilizzo di pacchetti (documentazione di NuGet). Confermare le versioni corrette del pacchetto all'indirizzo NuGet.org.

Inviare richieste con HttpClient

Inviare una richiesta 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("/");

    ...
}

Asserire il risultato. Prima di tutto, creare un'asserzione l'opposto del risultato previsto. Un'esecuzione iniziale con un'asserzione falsa positiva conferma che il test ha esito negativo quando il middleware viene eseguito correttamente. Eseguire il test e verificare che il test non riesca.

Nell'esempio seguente, il middleware deve restituire un codice di stato 404 (Non trovato) quando viene richiesto l'endpoint radice. Eseguire il primo test con Assert.NotEqual( ... );, che dovrebbe avere esito negativo:

[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);
}

Modificare l'asserzione per testare il middleware in condizioni operative normali. Il test finale usa Assert.Equal( ... );. Eseguire di nuovo il test per verificare che venga superato.

[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);
}

Inviare richieste con HttpContext

Un'app di test può anche inviare una richiesta usando SendAsync(Action<HttpContext>, CancellationToken). Nell'esempio seguente vengono eseguiti diversi controlli quando https://example.com/A/Path/?and=query viene elaborato dal 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 consente la configurazione diretta di un HttpContext oggetto anziché l'uso delle HttpClient astrazioni. Usare SendAsync per modificare le strutture disponibili solo nel server, ad esempio HttpContext.Items o HttpContext.Features.

Come per l'esempio precedente testato per una risposta 404 - Non trovato , controllare l'opposto per ogni Assert istruzione nel test precedente. Il controllo conferma che il test ha esito negativo correttamente quando il middleware funziona normalmente. Dopo aver confermato che il test falso positivo funziona, impostare le istruzioni finali Assert per le condizioni e i valori previsti del test. Eseguirlo di nuovo per verificare che il test venga superato.

Aggiungere route di richiesta

È possibile aggiungere route aggiuntive tramite la configurazione usando il test 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);

È anche possibile aggiungere route aggiuntive usando l'approccio server.SendAsync.

Limitazioni di TestServer

TestServer:

  • È stato creato per replicare i comportamenti del server per testare il middleware.
  • Non tenta di replicare tutti i HttpClient comportamenti.
  • Tenta di concedere al client l'accesso al maggior numero possibile di controllo sul server e con la massima visibilità su ciò che accade sul server il più possibile. Ad esempio, può generare eccezioni non generate normalmente da HttpClient per comunicare direttamente lo stato del server.
  • Per impostazione predefinita, alcune intestazioni specifiche del trasporto non sono in genere rilevanti per il middleware. Per ulteriori informazioni, vedi la sezione successiva.
  • Ignora la Stream posizione passata tramite StreamContent. HttpClient invia l'intero flusso dalla posizione iniziale, anche quando viene impostato il posizionamento. Per altre informazioni, vedere questo problema in GitHub.

Intestazioni Content-Length e Transfer-Encoding

TestServer non imposta intestazioni di richiesta o risposta correlate al trasporto, ad esempio Content-Length o Transfer-Encoding. Le applicazioni devono evitare di dipendere da queste intestazioni perché l'utilizzo varia in base a client, scenario e protocollo. Se Content-Length e Transfer-Encoding sono necessari per testare uno scenario specifico, possono essere specificati nel test durante la composizione di HttpRequestMessage o HttpContext. Per altre informazioni, vedere i problemi di GitHub seguenti: