【问题标题】:Unit Testing ASP.NET Core HttpResponse.OnStarting()单元测试 ASP.NET Core HttpResponse.OnStarting()
【发布时间】:2018-04-09 19:21:57
【问题描述】:

我有一个 ASP.NET Core 中间件,它负责将标头添加到响应中。在遵循最佳实践中,我在HttpResponse.OnStarting(Func<Task>) 的上下文中执行标头更改,以确保在响应刷新到客户端之前立即执行回调。

public class ResponseHeadersMiddleware
{
    private readonly RequestDelegate _next;

    public ResponseHeadersMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        context.Response.OnStarting(() =>
        {
            context.Response.Headers.Add("X-Some-Header", "Foobar");

            return Task.CompletedTask;
        });

        // Pass the request through the pipeline 
        await _next(context);
    }
}

这按预期工作,但我不确定如何最好地为这个实际触发 HttpResponse.OnStarting() 的中间件编写单元测试。我唯一能想到的是使用Microsoft.AspNetCore.TestHost 构建一个TestServer,它集成了中间件并执行完整的请求管道。虽然功能齐全,但这更像是一个集成测试,而不是真正的单元测试。

[Fact]
public async Task when_adding_response_headers()
{
    // ARRANGE
    var subject = new TestServer(new WebHostBuilder()
        .UseStartup<TestStartup<ResponseHeadersMiddleware>>());

    // ACT
    var response = await subject.CreateClient()
        .SendAsync(new HttpRequestMessage(HttpMethod.Get, "/")); // middleware fires for all requests

    // ASSERT
    Assert.True(response.Headers.TryGetValues("X-Some-Header", out var someHeader));
    Assert.Equals("Foobar", someHeader.FirstOrDefault()
}

private class TestStartup<TMiddleware> where TMiddleware : class
{
    public void ConfigureServices(IServiceCollection services)
    {
        RequestDelegate requestDelegate = context => Task.FromResult(0);

        services.AddSingleton(requestDelegate);
        services.AddSingleton<TMiddleware>();
    }

    public void Configure(IApplicationBuilder app)
    {
        dynamic middleware = app.ApplicationServices.GetService(typeof(TMiddleware));

        app.Use(async (ctx, next) =>
        {
            await middleware.Invoke(ctx);
            await next();
        });
    }
}

有没有办法在传递给我的中间件的 HttpContext 上触发 HttpResponse.OnStarting(),而无需端到端集成测试?

【问题讨论】:

  • 这是一个有趣的问题。您是否考虑过模拟一个响应对象并将其与测试中使用的 HttpContext 一起使用。
  • 是的。在模拟/假 HttpContext 上调用 Invoke!从你那里的集成测试来看,它的优雅程度要低得多,而且它都是在内存中完成的,所以无论如何都会超级放大。好问题。有很多东西我想用 aspnetcore 进行测试,但如果没有类似地设置 testhost 就不行!
  • @Mardoxx 在查看了DefaultHttpContext、DefaultHttpResponse 和HttpResponseFeature 的来源之后,我还没有确定传递给HttpResponse.OnStarting() 的回调是如何被调用的。 github.com/aspnet/HttpAbstractions/blob/dev/src/…github.com/aspnet/HttpAbstractions/blob/dev/src/…github.com/aspnet/HttpAbstractions/blob/dev/src/…
  • 您需要测试该响应是否包含预期的标头 - 所以像测试整个管道一样测试它。 在查看...的源代码之后 - 您真的希望您的测试依赖于实现细节吗?不管你如何对测试“集成”或“单元”进行分类——它们都是断言你的应用程序按预期工作的测试,并且应该只按执行时间分类——>“慢”和“快”

标签: c# unit-testing asp.net-core httpresponse


【解决方案1】:

在存储库中进行了一些挖掘并查看了他们的一些测试后,我想出了这个想法来利用受控上下文的响应功能。

这意味着要找到一种方法来捕获传递给OnStarting 的回调。决定尝试通过虚拟响应功能来获得它。

private class DummyResponseFeature : IHttpResponseFeature {
    public Stream Body { get; set; }

    public bool HasStarted { get { return hasStarted; } }

    public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();

    public string ReasonPhrase { get; set; }

    public int StatusCode { get; set; }

    public void OnCompleted(Func<object, Task> callback, object state) {
        //...No-op
    }

    public void OnStarting(Func<object, Task> callback, object state) {
        this.callback = callback;
        this.state = state;
    }
    
    bool hasStarted = false;
    Func<object, Task> callback;
    object state;
    
    public Task InvokeCallBack() {
        hasStarted = true;
        return callback(state);
    }
}

在测试中,我会在HttpContext 上设置功能,然后直接测试中间件。

[Fact]
public async Task when_adding_response_headers() {
    // ARRANGE
    var feature = new DummyResponseFeature();
    var context = new DefaultHttpContext();
    context.Features.Set<IHttpResponseFeature>(feature);
    
    RequestDelegate next = async (ctx) => {
        await feature.InvokeCallBack();
    };
    
    var subject = new ResponseHeadersMiddleware(next);

    // ACT
    await subject.Invoke(context);

    // ASSERT
    var response = context.Response;
    Assert.True(response.Headers.TryGetValues("X-Some-Header", out var someHeader));
    Assert.Equals("Foobar", someHeader.FirstOrDefault()
}

当请求委托在中间件中等待时,让请求委托调用捕获回调

【讨论】:

  • 我更喜欢这个,尽管为测试这个组件保留的实现复杂性可能超过了让测试“纯”的价值。遗憾的是代码不利于单元测试。 :(
  • @NathanTaylor 从技术上讲,您可以使用您选择的模拟框架轻松模拟界面和上下文。我选择使用已经存在的东西,并以他们如何测试自己的中间件为例。在测试设置中节省了很多。仅使用必要的依赖项单独执行测试对象。
  • 我必须用public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();初始化标题
  • @Kiechlus 好一个。我更新了答案以包含您的发现。
  • 感谢您提供这个优雅的解决方案
【解决方案2】:

使用 NSubstitute,这可以在没有帮助器类的情况下完成:

 HttpContext httpContext = new DefaultHttpContext();
 IHttpResponseFeature mockReponseFeature = Substitute.For<IHttpResponseFeature>();

 mockReponseFeature.OnStarting(Arg.Do<Func<object, Task>>(f => f(httpContext)), Arg.Any<object>());
 httpContext.Features.Set<IHttpResponseFeature>(mockReponseFeature);

 var middleware = new ResponseHeadersMiddleware(next: async (innerHttpContext) => { await Task.CompletedTask; });

 await middleware.Invoke(httpContext);

 httpContext.Response.Headers.Received(1).Add("X-Some-Header", "Foobar");

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-07-28
    • 2020-11-13
    • 1970-01-01
    • 2018-05-01
    • 2010-10-10
    • 2017-03-22
    • 2018-05-26
    • 2019-07-20
    相关资源
    最近更新 更多