Dotnet core 2.x mocking HttpContext etc.
Unit Test or Integration Test?
Unit testing and integration testing are two very black-and-white concepts - on paper! A Unit Test for an MVC action should call the code as directly as possible, injecting any mocks into either the controller constructor and/or the action - easy right?Not really! What if you access request, response etc? These should all be injected into the controller right? That would make sense and make it much easier to test writing of headers, reading of request parameters etc.
No, you need an integration test right? Integration testing means testing the application with things joined together: databases wired up, services in place and these can be automated too. Except that dotnet core doesn't (obviously) provide a way to inject any mocks. For integration testing, you shouldn't use mocks, which brings me back to the original problem.
When testing special actions like uploading files, multi-part forms etc. I need to access the context and in one case, the response since I am writing a file to the response directly. I also need to mock certain other services because I do not want to wire everything up, just to check that the things I think I am setting are actually being set!
How do we mock HttpContext etc. in our dotnet core unit tests?
Things to know first:- HttpContext is quite complicated!
- Not all properties have setters, some have to be set indirectly
- ControllerBase does not use the injected IHttpContextAccessor for its Context property
- They use this weird Features mechanism to attach data
- Classes like DefaultHttpResponse have a reference to their parent object (the context), which creates a slight chicken-and-egg problem.
- In my example, I use DI to get my controller instance but you could instead create one directly and pass the service mocks into the constructor yourself.
So here's what I did, using Moq for mocks and in this case, just providing a concrete response object so that my action could set response headers and query a client ip address without falling over. You could easily extend this for request objects etc:
httpContext = new Mock();
httpContext.Setup(ct => ct.Connection.RemoteIpAddress )
.Returns(new System.Net.IPAddress(0x2414188f));
contextAccessor = new Mock();
contextAccessor.Setup(ca => ca.HttpContext)
.Returns(httpContext.Object);
var features = new FeatureCollection();
features.Set(new HttpResponseFeature());
httpContext.Setup(ct => ct.Features)
.Returns(features);
var response = new DefaultHttpResponse(httpContext.Object);
httpContext.Setup(ct => ct.Response)
.Returns(response);
var controller = ActivatorUtilities.CreateInstance(services);
controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = contextAccessor.Object.HttpContext;
var result = await controller.GetImage(new GetImageModel { accesstoken = VALID_ACCESS_TOKEN, imagename = VALID_IMAGE_NAME}) as FileStreamResult;