Saturday, November 21, 2015

How to easily test MVC Controller Actions that use HttpContext

If you're like me, you've heard how awesome MVC is at being testable, but then you see everyone going off and using HttpContext.Request and other methods that make unit testing seemingly impossible or very difficult.  I mean, what's the point of crowing about MVC being testable if it's not very easy to test your code that uses the basic functionality of the framework?  I work on several very large MVC applications and everyone uses HttpContext all over the place, and rightly so.  I was trying to write all kinds of crazy code to try to abstract, inject, etc.  Something just felt wrong.

Well, after much banging my head against the wall, I've found a way to test actions that doesn't require you to stop using HttpContext.  Do realize that this will not fix your code using the static property System.Web.HttpContext.Current.

Let's see some code

Let's go ahead and create a fresh MVC project in Visual Studio 2015.  Make sure to have it create a test project for you, too.
Navigate to the HomeController and make the following changes:

public ActionResult About() {
ViewBag.Message = "Your application description page.";
       ViewBag.Url = HttpContext.Request.RawUrl;
return View();
}

Add the following line in the About view:
@{
    ViewBag.Title = "About";
}
<h2>@ViewBag.Title.</h2>
<h3>@ViewBag.Message</h3>
<p>You navigated to this url: @ViewBag.Url</p>

<p>Use this area to provide additional information.</p>

Run the project and notice that it displays:




Good, now let's modify the test to make sure that the URL is what we think it should be.
Navigate to the HomeControllerTest file and add the following:

[TestMethod]
public void About() {
// Arrange
HomeController controller = new HomeController();
// Act
ViewResult result = controller.About() as ViewResult;
// Assert
Assert.AreEqual("Your application description page.", result.ViewBag.Message);
Assert.AreEqual("http://localhost:59191/Home/About", result.ViewBag.RawUrl);
}

Now run the test and notice: BANG!
NullReferenceException: Object reference not set to an instance of an object.



It makes sense--you're not in Kansas anymore.  And by Kansas, of course, I mean IIS...or whatever baked my fresh HttpContext before.  So, HttpContext is null. :'(

MvcContrib Test Helper

Install the following package using the NuGet Package Manager:

  1. Right-click the References special folder for the test project and click Manage NuGet Packages...
  2. Search for MvcContrib.TestHelper
  3. Install

Update the test method and run the About test again


using MvcContrib.TestHelper;
...
[TestMethod]
public void About() {
// Arrange
var controller = new HomeController();
var builder = new TestControllerBuilder();
builder.RawUrl = "http://localhost:59191/Home/About";
builder.InitializeController(controller);
// Act
var result = controller.About() as ViewResult;
// Assert
Assert.AreEqual("Your application description page.", result.ViewBag.Message);
Assert.AreEqual("http://localhost:59191/Home/About", result.ViewBag.Url);
}

Success!!!


That's great, but what if I wanted to use Url instead of RawUrl?

I have to admit, I originally wrote this post with Request.Url in mind.  It didn't work--there was no builder.Url hanging around--just RawUrl.  But, I did find a way of making that work, too!

Here's the controller code:
public ActionResult About() {
ViewBag.Message = "Your application description page.";
ViewBag.RawUrl = HttpContext.Request.RawUrl;
ViewBag.Url = HttpContext.Request.Url;
return View();
}

Here's the Test code:
using Rhino.Mocks; //This gets installed with MvcContrib Test Helper
...
[TestMethod]
public void About() {
// Arrange
var controller = new HomeController();
var builder = new TestControllerBuilder();
builder.RawUrl = "http://localhost:59191/Home/About";
builder.InitializeController(controller);
builder.HttpContext.Request.Stub(x => x.Url).Return(new Uri("http://localhost:59191/Home/About"));
// Act
var result = controller.About() as ViewResult;
// Assert
Assert.AreEqual("Your application description page.", result.ViewBag.Message);
Assert.AreEqual("http://localhost:59191/Home/About", result.ViewBag.RawUrl);
Assert.AreEqual("http://localhost:59191/Home/About", result.ViewBag.Url.ToString());
}

Run the test:
BAM! SCORE!

It gets better!

Here's a list of properties hanging off of TestControllerBuilder just begging to be used:
  • Files
  • Form
  • HttpContext
  • PathInfo
  • QueryString
  • RawUrl
  • RouteData
  • Session
  • TempDataDictionary
Here are a few good references on how to use the other features of MvcContrib TestHelper:

Summary

  • MVC is testable
  • We all like to use HttpContext
  • HttpContext is a pain to set up and inject for testing controller actions
  • If you do nothing, HttpContext, Request, Response, etc. will all be null
  • Idea: Use MvcContrib Test Helper
  • Use its easy-access properties where you can
  • Use RhinoMocks to fill in the holes
Do you have something awesome to add to this post?  Great!  Leave a comment.
Do you know of a better way?  Great! Leave a comment.
Did I do something that you hate?  Great!  Leave a comment.
The more we engage, the better we'll all be at our jobs.

Thank you for reading, sharing, and engaging--and God bless!
Brandon