测试 ASP.NET 核心中间件

作者:Chris Ross

中间件可以使用TestServer进行独立测试。 该功能允许:

  • 您可以创建一个只包含需要测试的组件的应用管道。
  • 发送自定义请求以验证中间件行为。

优点:

  • 请求在内存中发送,而不是通过网络序列化。
  • 这可以避免其他问题,例如端口管理和 HTTPS 证书。
  • 中间件中的异常可以直接流回调用测试。
  • 可以直接在测试中自定义服务器数据结构,例如 HttpContext

设置 TestServer

在测试项目中,创建一个测试:

  • 创建并启动使用 TestServer 的主机。

  • 添加中间件使用的任何必需服务。

  • 向项目添加对 Microsoft.AspNetCore.TestHost NuGet 包的包引用。

  • 将处理管道配置为使用中间件进行测试。

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

注释

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

使用 HttpClient 发送请求

发送请求使用 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("/");

    ...
}

断言结果。 首先,断言与预期的结果相反。 使用假阳性断言的初始运行确认了当中间件正确执行时测试会失败。 运行测试并确认测试失败。

在以下示例中,当请求根终结点时,中间件应返回 404 状态代码(找不到)。 运行第一个测试 Assert.NotEqual( ... );时,应失败:

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

更改断言以在正常运行条件下测试中间件。 最终测试使用 Assert.Equal( ... );。 再次运行测试以确认它通过。

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

使用 HttpContext 发送请求

测试应用还可以使用 SendAsync(Action<HttpContext>、CancellationToken)发送请求。 在以下示例中,当 https://example.com/A/Path/?and=query 被中间件处理时,会进行多个检查:

[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 允许直接配置 HttpContext 对象,而不是使用 HttpClient 抽象。 使用 SendAsync 来操作仅在服务器上可用的结构,例如 HttpContext.ItemsHttpContext.Features

与针对 404 - 未找到 响应进行测试的前面示例一样,请在前面的测试中检查每个 Assert 语句的相反情况。 检查确认测试在中间件正常运行时是否正确失败。 确认误报测试有效后,为测试的预期条件和值设置最终 Assert 语句。 再次运行它以确认测试通过。

添加请求路由

可以通过使用测试 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);

还可以使用此方法 server.SendAsync添加其他路由。

TestServer 限制

TestServer:

  • 已创建用于复制服务器行为以测试中间件。
  • 尝试复制所有HttpClient行为。
  • 尝试使客户端能够尽可能多地控制服务器,并尽可能多地了解服务器上发生的情况。 例如,HttpClient 可能会引发一些它通常不引发的异常,以便直接传达服务器状态。
  • 默认情况下不会设置某些传输特定标头,因为这些标头通常与中间件无关。 有关详细信息,请参阅下一节。
  • 忽略StreamContent所传递的Stream位置。 HttpClient 从开始位置发送整个流,即使设置了定位也是如此。 有关详细信息,请参阅此 GitHub 问题

内容长度和传输编码标头

TestServer 设置传输相关的请求或响应标头,如 Content-LengthTransfer-Encoding。 应用程序应避免依赖于这些标头,因为它们的使用因客户端、方案和协议而异。 如果Content-LengthTransfer-Encoding是测试特定情况所必需的,可以在编写HttpRequestMessageHttpContext时在测试中指定它们。 有关详细信息,请参阅以下 GitHub 问题: