Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit Testing ASP.NET Core HttpResponse.OnStarting()

I have an ASP.NET Core middleware which is responsible for adding headers to a response. In following best practices, I am executing the header changes in the context of HttpResponse.OnStarting(Func<Task>), which ensures callback execution immediately before the response is flushed to the client.

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

This works as-intended, but I am not sure how best to write a unit test for this middleware which actually fires HttpResponse.OnStarting(). The only thing I could come up with was using a Microsoft.AspNetCore.TestHost to build a TestServer which integrates the middleware and executes the full request pipeline. While functional, this is more an integration test, than a true unit test.

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

Is there a way to trigger HttpResponse.OnStarting() on the HttpContext passed to my middleware, without an end-to-end integration test?

like image 684
Nathan Taylor Avatar asked Apr 09 '18 19:04

Nathan Taylor


1 Answers

After some digging around in the repository and looking at a few of their tests I came up with this idea to tap into the response feature of a controlled context.

That meant finding a way to capture the callback passed to OnStarting. Decided to try and get it through a dummy response feature.

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

Within the test I would set the feature on the HttpContext and then test the middle ware directly.

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

having the request delegate invoke captured the callback when the request delegate is awaited in the middleware

like image 166
Nkosi Avatar answered Oct 26 '22 20:10

Nkosi