Unit Testing ASP.NET Core HttpResponse.OnStarting()
Asked Answered
G

3

5

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?

Grice answered 9/4, 2018 at 19:21 Comment(4)
This is an interesting question. have you considered mocking a response object and using it with the HttpContext used in the test.Residuum
Yes. Call Invoke on a mock/fake HttpContext! Rather a lot less elegant from the integration test you have there also it is all done in memory so will be super zoomy anyways. Good question. There are a lot of things I'd like to test with aspnetcore but can't without similarly setting up the testhost!Lederman
@Lederman After reviewing the source of DefaultHttpContext, DefaultHttpResponse and HttpResponseFeature, I have yet to determine how the callback passed to HttpResponse.OnStarting() gets invoked. github.com/aspnet/HttpAbstractions/blob/dev/src/… github.com/aspnet/HttpAbstractions/blob/dev/src/… github.com/aspnet/HttpAbstractions/blob/dev/src/…Grice
You need to test that response contains expected header - so test it as you did with testing whole pipeline. After reviewing the source of ... - you really want that your tests relay on implementation details? Doesn't matter how you classify tests "integration" or "unit" - they are all tests that assert your application works as expected and should be categorised only by time of execution -> "slow" and "quick"Joviality
R
12

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

Residuum answered 9/4, 2018 at 20:30 Comment(5)
I like this more, though the amount of implementation complexity reserved for testing this component perhaps outweighs the value of having the test be "pure". Shame that the code isn't more conducive to unit testing. :(Grice
@NathanTaylor technically you could just as easily mocked the interface and the context with a mocking framework of your choice. I choose to use what already existed and also taking example from how they tested their own middlewares. Saves alot in test set up. The subject under test was exercised in isolation using just the necessary dependencies.Residuum
I had to initialize the headers with public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();Brisbane
@Brisbane good one. I updated answer to include your findings.Residuum
thank you for this elegant solutionCincinnati
M
0

With NSubstitute, this could be done without the helper class:

 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");
Monthly answered 7/8, 2019 at 20:21 Comment(0)
M
0

I didn't like having to build a dummy feature class to get this done so I mocked out the IHttpFeature. In this case I needed to test the response headers, but in the real case the OnStarting doesn't get called right away when registering the delegate. Not crazy about having to store and call it myself but it works.

var httpContext = new DefaultHttpContext();
Mock<IHttpResponseFeature> feature = new Mock<IHttpResponseFeature>();
feature.Setup(x => x.Headers).Returns(new HeaderDictionary()); httpContext.Features.Set(feature.Object);
//other code to set up
Func<object, Task> onStartingFunc = (obj) => { throw new Exception("wrong place wrong time"); };
object onStartingObject = new object();
feature.Setup(x => x.OnStarting(It.IsAny<Func<object, Task>>(), It.IsAny<object>()))
                .Callback((Func<object, Task> callback, object obj) =>
                {
                    onStartingFunc = callback;
                    onStartingObject = obj;
                }
                );

 var requestDelegate = new RequestDelegate(
            async (innerContext) =>
            {
                //other setup code
                await Task.FromResult(0);
            });

 await sut.InvokeAsync(httpContext, requestDelegate);
// invoke your callback
 await onStartingFunc(onStartingObject);
Mebane answered 28/10, 2022 at 20:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.