Unit Testing Controller Actions in MVC 5

Visual Studio 2013

Problem #1 (Locating System.Web.Mvc 5.0)

In order to unit test controller actions in MVC 5 you need to have System.Web.Mvc added as a reference to your unit test project. However, you can no longer do this from the “add reference..” menu as it only goes up to version 4.0. You need version 5.0 to test MVC 5 controllers.

Solution:

In order to add this assembly to your unit test project you will need to open up Nuget (Right click project -> Manage Nuget packages). In here search for “Microsoft ASPNET MVC“. Install this package. You should now see the System.Web.Mvc 5.0 assembly amongst your list of referenced assemblies.

Problem #2 (Referencing ViewBag in Unit Tests)

When you go to unit test your viewbags, for example:

Assert.AreEqual(ReturnURL, result.ViewBag.ReturnUrl, "Return URLs Dont match");

You will see the following error:

“One or more types required to compile a dynamic expression cannot be found. Are you missing a reference?”

Solution:

In order to use a ViewBag dynamically you need to add an assembly using the “Add Reference…” Dialog.

The assembly to add is: Microsoft.CSharp v4.0.

Once this is added the error will no longer appear.


Problem #3 (Unit Testing AccountController)

The AccountController class been completely redesigned for MVC 5. It now uses a class called UserManager to handle a lot of the basic login and registration tasks. The problem is UserManager does not inherit from an interface and so it is difficult to unit test the AccountController actions.

Solution:

There are a number of ways to go here. The option I recommend is to remove the UserManager and any other low level code such as creating and updating user account details from the AccountController and put that code into a new class. I recommend putting this class in a separate project along with your Identity Context class. Once you have completed this class you will then swap out any of that original code in the AccountController with reference to your new class methods. These methods won’t do anything special, they will just execute the same code that was in the AccountController but is now in your new class. Once you have done this you can extract an interface for your new class and declare it in your AccountController constructor.

Remember that when you specify interfaces in your controller constructors you will need to set up dependency injection of some sort (I recommend ninject).

You will then be able to use your new interface within your unit tests, either by creating dummy classes from it or using a mocking framework of some kind (I recommend Moq).

Problem #4 (Using RouteValues dictionary)

When trying to assert values returned within an action result we utilise the ActionResults RouteValueDictionar, e.g.

Assert.AreEqual(“Index”, result.RouteValues[“action”],”Unexpected View Name”);

However, doing so in your MVC 5 unit test project may throw the following compile error:

Cannot apply indexing with [] to an expression of type ‘System.Web.Routing.RouteValueDictionary’

Solution:

Add System.Web.Routing 4.0 and System.Web 4.0 from the “Add Reference” dialog. Routing is dependent on system.web. Add the System.Web.Routing using declaration to your class file.

Problem #5 (Unit Test Controller not fully instantiated)

When you create an instance of a controller in a unit test class many of its objects are not created. Once such object is the UrlHelper object. This object is used extensively to help determine which url to redirect the user to once an action is complete. When your unit test hits a line of code that utilises the UrlHelper it will throw an null object exception.

Solution:

Setting up an instance of a UrlHelper is no easy feat. It involves first creating request, response, context and route objects. Below you can see my implementation:

protected UrlHelper SetFakeControllerHttpContext()
{
RouteCollection Routes;
Mock<HttpResponseBase> Response;
Mock<HttpRequestBase> Request;
Mock<HttpContextBase> Context;

Routes = new RouteCollection();
RouteConfig.RegisterRoutes(Routes);

Request = new Mock<HttpRequestBase>(MockBehavior.Strict);
Request.SetupGet(x => x.ApplicationPath).Returns("/");
Request.SetupGet(x => x.Url).Returns(new Uri("http://localhost/", UriKind.Absolute));
Request.SetupGet(x => x.ServerVariables).Returns(new System.Collections.Specialized.NameValueCollection());

Response = new Mock<HttpResponseBase>(MockBehavior.Strict);

Context = new Mock<HttpContextBase>(MockBehavior.Strict);
Context.SetupGet(x => x.Request).Returns(Request.Object);
Context.SetupGet(x => x.Response).Returns(Response.Object);
return new UrlHelper(new RequestContext(Context.Object, new RouteData()), Routes);
}

And here it is being used by the Controller inside my Constructor:

Controller.Url = SetFakeControllerHttpContext();