Testen von ASP.NET Core-Middleware

Von Chris Ross

Middleware kann mit TestServer isoliert getestet werden. Die Funktion ermöglicht Folgendes:

  • Instanziieren Sie eine App-Pipeline, die nur die Komponenten enthält, die Sie testen müssen.
  • Senden Sie benutzerdefinierte Anforderungen, um das Verhalten der Middleware zu überprüfen.

Vorteile:

  • Anforderungen werden arbeitsspeicherintern gesendet und nicht über das Netzwerk serialisiert.
  • Dadurch werden zusätzliche Aspekte wie Portverwaltung und HTTPS-Zertifikate vermieden.
  • Ausnahmen in der Middleware können direkt an den aufrufenden Test zurückfließen.
  • Es ist möglich, Serverdatenstrukturen, wie z. B. HttpContext, direkt im Test anzupassen.

Einrichten von TestServer

Erstellen Sie im Testprojekt einen Test:

  • Erstellen und starten Sie einen Host, der TestServer verwendet.

  • Fügen Sie alle benötigten Dienste hinzu, die die Middleware verwendet.

  • Fügen Sie dem Projekt einen Paketverweis für das NuGet-Paket Microsoft.AspNetCore.TestHost hinzu.

  • Konfigurieren Sie die Verarbeitungspipeline für die Nutzung der Middleware für den 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();
    
        ...
    }
    

Hinweis

Einen Leitfaden zum Hinzufügen von Paketen zu .NET-Apps finden Sie in Installieren und Verwalten von Paketen unter Workflow der Nutzung von Paketen (NuGet-Dokumentation). Überprüfen Sie unter NuGet.org, ob die richtige Paketversion verwendet wird.

Senden von Anforderungen mit HttpClient

Senden Sie eine Anforderung mit 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("/");

    ...
}

Bestätigen Sie das Ergebnis. Machen Sie zunächst eine Assertion, die das Gegenteil des von Ihnen erwarteten Ergebnisses darstellt. Ein erster Lauf mit einer falsch-positiven Assertion bestätigt, dass der Test fehlschlägt, wenn die Middleware einwandfrei funktioniert. Führen Sie den Test aus, und bestätigen Sie, dass der Test fehlschlägt.

Im folgenden Beispiel sollte die Middleware den Statuscode 404 (Nicht gefunden) zurückgeben, wenn der Stammendpunkt angefordert wird. Führen Sie den ersten Testlauf mit Assert.NotEqual( ... ); durch, der fehlschlagen sollte:

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

Ändern Sie die Assertion so, dass die Middleware unter normalen Betriebsbedingungen getestet wird. Im letzten Test wird Assert.Equal( ... ); verwendet. Führen Sie den Test erneut aus, um zu bestätigen, dass er bestanden wird.

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

Senden von Anforderungen mit HttpContext

Eine Test-App kann eine Anforderung auch mit SendAsync(Action<HttpContext>, CancellationToken) senden. Im folgenden Beispiel erfolgen mehrere Prüfungen, wenn https://example.com/A/Path/?and=query von der Middleware verarbeitet wird:

[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 erlaubt die Direktkonfiguration eines HttpContext-Objekts, anstatt die HttpClient-Abstraktionen zu verwenden. Verwenden Sie SendAsync, um Strukturen zu manipulieren, die nur auf dem Server verfügbar sind, wie z. B. HttpContext.Items oder HttpContext.Features.

Wie im früheren Beispiel, das auf eine Antwort des Typs 404 – nicht gefunden getestet wurde, überprüfen Sie das Gegenteil für jede Assert-Anweisung im vorhergehenden Test. Die Überprüfung bestätigt, dass der Test bei normalem Betrieb der Middleware ordnungsgemäß fehlschlägt. Nachdem Sie bestätigt haben, dass der falsch positive Test funktioniert, legen Sie die endgültigen Assert-Anweisungen für die erwarteten Bedingungen und Werte des Tests fest. Führen Sie ihn erneut aus, um zu bestätigen, dass er bestanden wird.

Anforderungsrouten hinzufügen

Zusätzliche Routen können durch Konfiguration mithilfe des Tests HttpClient hinzugefügt werden:

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

Zusätzliche Routen können auch mithilfe des Ansatzes server.SendAsync hinzugefügt werden.

TestServer-Einschränkungen

TestServer:

  • Wurde erstellt, um Serververhalten zum Testen von Middleware zu replizieren.
  • Hierbei wird nicht versucht, das gesamte HttpClient-Verhalten zu replizieren.
  • Es wird versucht, dem Client so viel Kontrolle über den Server wie möglich zu geben, mit weitestgehendem Einblick in die Vorgänge auf dem Server. Beispielsweise können Ausnahmen ausgelöst werden, die normalerweise nicht von HttpClient ausgelöst werden, um den Serverzustand direkt zu kommunizieren.
  • Standardmäßig werden einige transportspezifische Header nicht gesetzt, da diese in der Regel für Middleware nicht relevant sind. Weitere Informationen finden Sie im nächsten Abschnitt.
  • Ignoriert die Stream Position, die StreamContent durchlaufen hat. HttpClient sendet den gesamten Datenstrom von der Startposition aus, auch wenn die Positionierung festgelegt ist. Weitere Informationen finden Sie in diesem GitHub-Issue.

Content-Length- und Transfer-Encoding-Header

TestServer legt keine transportbezogenen Anforderungs- oder Antwortheader wie Content-Length oder Transfer-Encoding fest. In Anwendungen sollten Abhängigkeiten von diesen Headern vermieden werden, weil ihre Verwendung je nach Client, Szenario und Protokoll variiert. Wenn Content-Length und Transfer-Encoding erforderlich sind, um ein bestimmtes Szenario zu testen, können sie im Test angegeben werden, wenn HttpRequestMessage oder HttpContext verfasst wird. Weitere Informationen finden Sie in den folgenden GitHub-Issues: