Introduction
When it comes to unit testing, it is common to use mocking to replace "external" services, that is, those that are not part of the subject under test. Not everything can be mocked, and, sometimes, when we instantiate non-POCO classes, such as controllers (ControllerBase), there are lots of things that are not instantiated by the framework and are left for us do to. Just think, for example, if you want to access request or quest string data or know if the user is logged in?
In this post I will talk about two things: the user identity and the request data.
The HttpContext
Inside of a controller action method, it is common to get information which actually comes from the the context (the HttpContext property):
All of these properties get routed to the HttpContext property, which is read-only. It turns out that this property is itself routed to the ControllerContext's property's own HttpContext, but the ControllerContext can be set:
var controller = new HomeController(); controller.ControllerContext = new ControllerContext();
So, if we want to set a controller's HttpContext property, all we have to do is set the ControllerContext's HttpContext:
controller.ControllerContext.HttpContext = new DefaultHttpContext();
The DefaultHttpContext class is the default implementation of the abstract HttpContext class that is included in ASP.NET Core.
Features
All of the .NET and ASP.NET Core is very modular and designed for extensibility, so it should come as no surprise that all of the HttpContext features are provided as features and it's easy to provide implementations for them.
Features are accessible through the Features collection and can be set using as key one interface, which is generally a core interface that identifies that feature:
context.Request.Set<IFormFeature>(new FormCollection(request));
The DefaultHttpContext takes in one of its constructors a collection of features, from which it can extract its functionality.
Request Data
When we talk about the request we can be talking about:
- Form data (including files)
- The query string
Each is implemented by its own feature. So, if we want to mock form data, we would do something like this, leveraging the IFormFeature:
var request = new Dictionary { { "email", "rjperes@hotmail.com" }, { "name", "Ricardo Peres" } }; var formCollection = new FormCollection(request); var form = new FormFeature(formCollection);
var features = new FeatureCollection(); features.Set<IFormFeature>(form); var context = new DefaultHttpContext(features);
This allows us to write code like this, from inside the :
var email = HttpContext.Request["email"];
As for the query string, we would do it like this, using IQueryFeature:
var request = new Dictionary { { "id", "12345" } }; var queryCollection = new QueryCollection(request); var query = new QueryFeature(queryCollection);
var features = new FeatureCollection(); features.Set<IQueryFeature>(form); var context = new DefaultHttpContext(features);
This code gives us the ability to do this:
var id = HttpContext.Request.Query["id"];
Response
What about sending responses, HTTP status codes and cookies? I've got you covered too! The feature contract for the response is IHttpResponseFeature and we add it like this:
var response = new HttpResponseFeature();
var features = new FeatureCollection(); features.Set<IHttpResponseFeature>(response); var context = new DefaultHttpContext(features);
var responseCookies = new ResponseCookiesFeature(context.Features);
Notice that the ResponseCookiesFeature is a little awkward, as it takes the feature collection in its constructor.
With this on, you can set the response status or some cookies on the response of your action:
Response.StatusCode = 400; Response.Cookies.Append("foo", "bar");
User Identity
As for the user identity, it's much simpler, we just need to create a ClaimsPrincipal with any claims that we want:
var context = new DefaultHttpContext(features)
{ User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, "<username>"),
new Claim(ClaimTypes.Role, "<role>")
})) };
You can just pass any username and roles you want, and they will be made available to the controller's User property.
Conclusion
As you can see, with a little bit of work, almost anything is possible in .NET Core, and, particularly, when it comes to unit tests! Stay tuned for more!