In my previous article I outlined a way to unit test HttpContext.Current
. You can read about that here:
Unit testing HttpContext.Current outside of a Controller
This was in code outside of a Controller. But what happens if we’re within a Controller? Inside a Controller HttpContext
is abstract because it is of type HttpContextBase
. This means we can mock it. But how do we achieve that? How do we hook into those internal Controller properties? We need to inject a mocked HttpContextBase
into our Controller somehow. We can do this by overriding the ControllerContext
. In this post I’ll share the NUnit template I use for testing Controllers. I'll show you how to create a fake HttpContext
. I'll also detail how to mock out the dependencies using NSubstitute
.
The first thing we need is our testing frameworks, which we’ll install via Nuget. Open the Nuget Package Manager Console. It lives here: Tools > Nuget Package Manager > Package Manager Console. We need to install NUnit and NSubstitute.
Install-Package NUnit
Install-Package NSubstitute
Now they’re installed, let’s look at a generic test template first. I use this for every test that isn't a Controller test. The Controller test template builds on this one so we need a quick look at this.
using NUnit.Framework;
[TestFixture]
public abstract class TestTemplate<TContext>
{
protected virtual TContext Sut { get; private set; }
[SetUp]
public virtual void MainSetup()
{
Sut = EstablishContext();
}
[TearDown]
public void Teardown()
{
TestCleanup();
}
protected abstract TContext EstablishContext();
protected abstract void TestCleanup();
}
There's a couple of interesting things going on here. We've got a property called Sut
. That just means Subject Under Test. It's a quick way of giving us access to the object that we're testing. When we create a test we have to override EstablishContext
. This returns an instance of the type we want to test and assigns it to Sut
. We've got a TestCleanup
method where we can perform any test clean-up we need. Notice also that our MainSetup
method is virtual. This allows us to override it in our ControllerTestTemplate
. Let's look at that right now.
using System;
using System.Collections;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using NSubstitute;
public abstract class ControllerTestTemplate<tcontext> : TestTemplate<tcontext> where TContext : Controller
{
private const string RootPath = "/basepath";
protected static Uri BaseUri
{
get { return new Uri("http://localhost:4000"); }
}
protected HttpContextBase HttpContextSub { get; private set; }
protected HttpRequestBase RequestSub { get; private set; }
protected HttpResponseBase ResponseSub { get; private set; }
public override void MainSetup()
{
base.MainSetup();
Sut.ControllerContext = FakeControllerContext(Sut);
Sut.Url = new UrlHelper(Sut.ControllerContext.RequestContext);
}
private ControllerContext FakeControllerContext(ControllerBase controller)
{
InitialiseFakeHttpContext();
var routeData = new RouteData();
routeData.Values.Add("key1", "value1");
return new ControllerContext(HttpContextSub, routeData, controller);
}
private void InitialiseFakeHttpContext(string url = "")
{
HttpContextSub = Substitute.For<HttpContextBase>();
RequestSub = Substitute.For<HttpRequestBase>();
ResponseSub = Substitute.For<HttpResponseBase>();
var serverUtilitySub = Substitute.For<HttpServerUtilityBase>();
var itemsSub = Substitute.For<IDictionary>();
HttpContextSub.Request.Returns(RequestSub);
HttpContextSub.Response.Returns(ResponseSub);
HttpContextSub.Server.Returns(serverUtilitySub);
HttpContextSub.Items.Returns(itemsSub);
serverUtilitySub.MapPath("/virtual").Returns("c:/absolute");
RequestSub.ApplicationPath.Returns(RootPath);
RequestSub.Url.Returns(BaseUri);
if (!string.IsNullOrEmpty(url))
{
RequestSub.AppRelativeCurrentExecutionFilePath.Returns(url);
}
}
}
A couple of things to note here. First up, I'm mocking the Items
property. This is because the AccountController
uses it within the GetOwinContext()
extension method. Also notice that I'm exposing properties like Request
, Response
and HttpContext
in the template. This allows us to use them in our tests if required, as we'll see in a moment. We can expose as many of the properties as we need for our tests. Making calls to Url.Action
that you need to test? Add a UrlHelper
property and initialise it in MainSetup
.
That's the templates covered. Now how do I use them?
Now that we have our template, let's write a couple of tests that uses it.
We'll write a simple test for now that illustrates the template in action. Let's say we have a controller action in our HomeController
for displaying custom 404 errors. Here's what it might look like:
public ActionResult Error404()
{
Response.StatusCode = (int)HttpStatusCode.NotFound;
Response.TrySkipIisCustomErrors = true;
if (Request.IsAjaxRequest())
{
return Json(new { message = "Not found." }, JsonRequestBehavior.AllowGet);
}
return View();
}
Without our template we get a NullReferenceException
on the first line. If we use our template, the error goes away. Not only that, but we can assert that the properties are correct on the Response
. Here's a couple of tests that we might write to verify that our Response
is correct.
using System.Net;
using NUnit.Framework;
using Painstorming.Web.Controllers;
[TestFixture]
public class HomeControllerTests : ControllerTestTemplate<homecontroller>
{
protected override HomeController EstablishContext()
{
return new HomeController();
}
protected override void TestCleanup()
{
}
[Test]
public void GivenAHomeController_WhenGettingFromTheError404Action_ThenTheResponseStatusCodeShouldBeNotFound()
{
Sut.Error404();
Assert.That(ResponseSub.StatusCode, Is.EqualTo((int) HttpStatusCode.NotFound));
}
[Test]
public void GivenAHomeController_WhenGettingFromTheError404Action_ThenTheResponseTrySkipIisCustomErrorsShouldBeTrue()
{
Sut.Error404();
Assert.That(ResponseSub.TrySkipIisCustomErrors, Is.True);
}
}
What did we just do?
There was quite a lot going on here. Let's recap:
- We installed a couple of Nuget packages to help with our testing
- We created a base
TestTemplate
for all of our unit tests
- We created a
ControllerTestTemplate
for our Controller tests
- We wrote a couple of tests for a Controller action that handles displaying a 404 error page
View original article