Search code examples
c#asp.net-coreintegration-testingxunitasp.net-core-2.2

How to test with Response.OnCompleted delegate in a finally block


I have the following netcore 2.2 controller method that I am trying to write an xUnit integration test for:

    private readonly ISoapSvc _soapSvc;
    private readonly IRepositorySvc _repositorySvc;

    public SnowConnectorController(ISoapSvc soapSvc, IRepositorySvc repositorySvc)
    {
        _soapSvc = soapSvc;
        _repositorySvc = repositorySvc;
    }

    [Route("accept")]
    [HttpPost]
    [Produces("text/xml")]
    public async Task<IActionResult> Accept([FromBody] XDocument soapRequest)
    {         
        try
        {
            var response = new CreateRes
            {
                Body = new Body
                {
                    Response = new Response
                    {
                        Status = "Accepted"
                    }
                }
            };

            return Ok(response);
        }
        finally
        {
            // After the first API call completes
            Response.OnCompleted(async () =>
            {
                // Run the close method
                await Close(soapRequest);
            });
        }
    }

The catch block runs and does the things it needs to, then the finally block runs and does things it needs to do after the request in the catch finishes per design.

Close has been both a private method . It started as a public controller method but I don't need to expose it for function so moved it to private method status.

Here's an integration test I started with the intention of just testing the try portion of the code:

    [Fact]
    public async Task AlwaysReturnAcceptedResponse()
    {
        // Arrange------

        //   Build mocks so that we can inject them in our system under tests constructor
        var mockSoapSvc = new Mock<ISoapSvc>();
        var mockRepositorySvc = new Mock<IRepositorySvc>();

        //   Build system under test(sut)
        var sut = new SnowConnectorController(mockSoapSvc.Object, mockRepositorySvc.Object);

        var mockRequest = XDocument.Load("..\\..\\..\\mockRequest.xml");

        // Act------

        //   Form and send test request to test system
        var actualResult = await sut.Accept(mockRequest);
        var actualValue = actualResult.GetType().GetProperty("Value").GetValue(actualResult);

        // Assert------

        //   The returned object from the method call should be of type CreateRes
        Assert.IsType<CreateRes>(actualValue); 

    }

I am super new to testing... I've been writing the test and feeling my way through the problem. I started by entering the controller method not really knowing where it would go. The test works through the try method, and then an exception is thrown once it hits the delegate in the finally block.

It looks like my test will have to run through to the results of the finally block unless there is a way to tell it to stop with the catch blocks execution?

That's fine, i'm learning, but the problem with that approach for me now is that the HttpResponse's Response.OnCompleted delegate in the finally block returns null when my test is running and I haven't been successful at figuring out what I can do to not make it null - because it is null, it throws this when my unit test is executing -

 System.NullReferenceException: 'Object reference not set to an instance of an object.'

*One thought that occurred was that if I was to make the private Close method a public controller method, and then make the Accept method not have the finally block, I could create a third controller method that does the try finally action by running the two controller methods and then just test the individual controller methods that are strung together with the third. However, it doesn't feel right because I would be exposing methods just for the sake of unit testing and I don't need Close to be exposed.

If the above idea is not the right approach, I am wondering what is, and if I just need to test through end to end, how I would get over the null httpresponse?

Any ideas would be appreciated. Thank you, SO community!

EDIT - Updated Test that works after the accepted answer was implemented. Thanks!

    [Fact]

    public async Task AlwaysReturnAcceptedResponse()
    {
        // Arrange------

        //   Build mocks so that we can inject them in our system under tests constructor
        var mockSoapSvc = new Mock<ISoapSvc>();
        var mockRepositorySvc = new Mock<IRepositorySvc>();

        //   Build system under test(sut)
        var sut = new SnowConnectorController(mockSoapSvc.Object, mockRepositorySvc.Object)
        {
            // Supply mocked ControllerContext and HttpContext so that finally block doesnt fail test
            ControllerContext = new ControllerContext
            {
                HttpContext = new DefaultHttpContext()
            }
        };

        var mockRequest = XDocument.Load("..\\..\\..\\mockRequest.xml");

        // Act------

        //   Form and send test request to test system
        var actualResult = await sut.Accept(mockRequest);
        var actualValue = actualResult.GetType().GetProperty("Value").GetValue(actualResult);

        // Assert------

        //   The returned object from the method call should be of type CreateRes
        Assert.IsType<CreateRes>(actualValue); 

    }

Solution

  • Curious what you are doing in the Close method against the input parameter. Does it have to happen after response is being sent? It might not always happen as you would expect, see here.

    Regardless though, during runtime asp.net core runtime sets a lot of properties on the controller including ControllerContext, HttpContext, Request, Response etc. But those won't be available in unit testing since there is no asp.net core runtime there. If you really want to test this, you'll have to mock them. Here is the ControllerBase source code.

    As we can see, ControllerBase.Response simply returns ControllerBase.HttpContext.Response, and ControllerBase.HttpContext is a getter from ControllerBase.ControllerContext. This means you'll have to mock a ControllerContext (and the nested HttpContext as well as HttpResponse) and assign it to your controller in the setup phase.

    Furthermore, the OnCompleted callback won't get called in unit test either. If you want to unit test that part, you'll have to trigger it manually.

    Personally I think it's too much hassle beside the open bug I mentioned above.

    I would suggest you move the closing logic (if it's really necessary) to a IDisposable scoped service and handle that in the Dispose instead - assuming it's not a computation heavy operation which can impact the response latency.