Writing Unit Tests For Controller Actions

Just a brief note on writing unit tests for controller actions. When your action has a call to RedirectToAction or RenderView (yeah, pretty much every action) be aware that these methods have dependencies on various context objects.

If you attempt to mock these objects, you sometimes also have to mock their dependencies and their dependencies' dependencies and so on, depending on what you are trying to test. This is why I wrote my post on Test Specific Subclasses. It provides an easier way to test some of these cases.

Some of these challenges are the nature of mocking and some of them are due to protected methods that we realize we should probably make public.

In this post, I want to demonstrate a couple of unit test techniques for testing controller actions for the CTP release of the ASP.NET MVC Framework. Remember, this is a CTP so all of this may change in the future. I will be compiling testing patterns into a longer document on unit testing patterns for ASP.NET MVC

Controller with RedirectToAction

Here is the really simple controller we’ll test

public class HomeController : Controller
{
  [ControllerAction]
  public void Index()
  {
    RenderView("Index");
  }

  [ControllerAction]
  public void About()
  {
    RedirectToAction("Index");
  }
}

We will test the About action.

Test Specific Subclass Approach

For the most part, when a test calls RedirectToAction, you just want to no-op that method call. But if you want to verify that the action that is being redirected to is the correct one, here's one way to test it using a test-specific subclass.

[Test]
public void VerifyAboutRedirectsToCorrectActionUsingTestSpecificSubclass()
{
  HomeControllerTester controller = new HomeControllerTester();
  controller.About();
  Assert.AreEqual("Index", controller.RedirectedAction
    , "Should have redirected to 'Index'.");
}

internal class HomeControllerTester : HomeController
{
  public string RedirectedAction { get; private set; }

  protected override void RedirectToAction(object values)
  {
    this.RedirectedAction = (string)values.GetType()
      .GetProperty("Action").GetValue(values, null);
  }
}

In this test I inherited from the controller I am testing, following the Test Specific Subclass pattern (Note: This pattern leaves a bad taste in some TDDers mouths. I am aware of that. I still like it. But I already know some of you don’t).

One thing that is really ugly is I had to resort to reflection to get the Action we are redirecting to. This testing scenario will be fixed in the next release. Just showing you how it is done now.

Mock Framework Approach

In this test, I will use RhinoMocks to test the same thing as above.

[Test]
public void VerifyAboutRedirectsToCorrectActionUsingMockViewFactory()
{
  RouteTable.Routes.Add(new Route
  {
    Url = "[controller]/[action]",
    RouteHandler = typeof(MvcRouteHandler)
  });

  HomeController controller = new HomeController();
    
  MockRepository mocks = new MockRepository();
  IHttpContext httpContextMock = mocks.DynamicMock<IHttpContext>();
  IHttpRequest requestMock = mocks.DynamicMock<IHttpRequest>();
  IHttpResponse responseMock = mocks.DynamicMock<IHttpResponse>();
  SetupResult.For(httpContextMock.Request).Return(requestMock);
  SetupResult.For(httpContextMock.Response).Return(responseMock);
  SetupResult.For(requestMock.ApplicationPath).Return("/");
  responseMock.Redirect("/Home/Index");

  RouteData routeData = new RouteData();
  routeData.Values.Add("Action", "About");
  routeData.Values.Add("Controller", "Home");
  ControllerContext contextMock = new 
    ControllerContext(httpContextMock, routeData, controller);
  mocks.ReplayAll();

  controller.ControllerContext = contextMock;
  controller.About();

  mocks.VerifyAll();
}

The mock test actually tests the final URL that we would be redirecting to. You can verify this test is actually testing what I say it will by changing the line with "/Home/Index" to something like "/Home/Index2" and see that the test does fail.

Controller With RenderView

Using the same controller class above, let’s write a test to make sure the correct view is rendered.

Using Test Specific Subclass

[Test]
public void VerifyIndexSelectsCorrectViewUsingTestSpecificSubclass()
{
  HomeControllerTester controller = new HomeControllerTester();
  controller.Index();
  Assert.AreEqual("Index", controller.SelectedViewName
    , "Should have selected 'Index'.");
}

internal class HomeControllerTester : HomeController
{
  public string SelectedViewName { get; private set; }
    
  protected override void RenderView(string viewName
    , string masterName, object viewData)
  {
    this.SelectedViewName = viewName;   
  }
}

Using a Mock Framework

UPDATE: Sorry, but the following test doesn’t work in the CTP. I had compiled it against an interim build and not the CTP version. Apologies. For this scenario, you pretty much have to use the subclass approach. We will make this better in the next CTP.

[Test]
public void VerifyIndexSelectsCorrectViewUsingMockViewFactory()
{
  MockRepository mocks = new MockRepository();
  IViewFactory mockViewFactory = mocks.DynamicMock<IViewFactory>();
  IView mockView = mocks.DynamicMock<IView>();
  IHttpContext httpContextMock = mocks.DynamicMock<IHttpContext>();

  HomeController controller = new HomeController();
  RouteData routeData = new RouteData();

  ControllerContext contextMock = new ControllerContext(httpContextMock
    , routeData, controller);

  Expect.Call(mockViewFactory.CreateView(contextMock, "Index"
    , string.Empty, controller.ViewData)).Return(mockView);
  Expect.Call(delegate { mockView.RenderView(null); }).IgnoreArguments();
    
  mocks.ReplayAll();

  controller.ControllerContext = contextMock;
  controller.ViewFactory = mockViewFactory;
  controller.Index();

  mocks.VerifyAll();
}

Please note that while the Rhino Mocks examples look like a lot of code, on a real project I would build up a custom set of Extension methods to effectively create a DSL (Domain Specific Language) for testing my controllers.

I’ve already started on this a bit. Hopefully together, we can build up a really nice library to make testing controllers much more fluid.

In the meanwhile, we will also evaluate the sticking points when it comes to writing tests and do our part to reduce the friction for TDD scenarios.

 

Tags: ,
[ad] Free Bug Tracking & Project Management Software Axosoft’s OnTime 2007 allows software development teams to collaborate on software projects by tracking everything from defects to enhancements to helpdesk incidents in one easy-to-use database driven by an intuitive Windows, Web or VS.NET Integrated UI. Get a Free Single-User License ($200 Value!)

What others have said

Requesting Gravatar... Scott Hanselman's Computer Zen Dec 09, 2007 9:49 PM
# ASP.NET 3.5 Extensions - plus MVC How-To Screencast
Requesting Gravatar... Scott Hanselman's Computer Zen Dec 09, 2007 9:54 PM
# ASP.NET 3.5 Extensions - plus MVC How-To Screencast
Requesting Gravatar... ASPInsiders Dec 09, 2007 10:04 PM
# ASP.NET 3.5 Extensions - plus MVC How-To Screencast
Darn that ScottGu, he's scooped me again . Just kidding. Around dinner time this evening we released
Requesting Gravatar... ScottGu's Blog Dec 09, 2007 10:06 PM
# ASP.NET 3.5 Extensions CTP Preview Released
Earlier today we released the first CTP preview of an &amp;quot;ASP.NET 3.5 Extensions&amp;quot; release that
Requesting Gravatar... Aaron Jensen Dec 09, 2007 10:39 PM
# re: Writing Unit Tests For Controller Actions
FWIW, the pattern is fine. It's a perfectly acceptable way to test things that are designed in such a way that they cannot be tested with more elegant, readable approaches that require less work :)
Requesting Gravatar... Bill Barry Dec 10, 2007 6:05 AM
# re: Writing Unit Tests For Controller Actions
I have been testing with that pattern for some time now; it is often shorter and easier to use than mocking. As a general rule, if you have to mock more than one object a test specific class is easier to follow and usually shorter.

I wouldn't recommend to use this class instead of mocking, rather I would say to use it alongside mocking to further enhance your tests. Even in classes which are designed as elegantly testable, sometimes the necessary logic gets complex enough (for example because of correct algorithms instead of naive ones like Jeff Atwood has been talking about) where you would be required to mock 2 or 3 of the dependencies in order to adequately test the function. Always use the right tool for the job. Sometimes mocks are; sometimes subclasses are.
Requesting Gravatar... Steven Harman Dec 10, 2007 6:54 AM
# re: Writing Unit Tests For Controller Actions
If you attempt to mock these objects, you sometimes also have to mock their dependencies and their dependencies' dependencies and so on, depending on what you are trying to test.


*sniff, sniff...* smells like a good opportunity for an Auto-Mocking (IoC) Container, yes? :)
Requesting Gravatar... Jason Haley Dec 10, 2007 7:26 AM
# Interesting Finds: December 10, 2007
Requesting Gravatar... Peli Dec 10, 2007 8:51 AM
# re: Writing Unit Tests For Controller Actions
Looks like an awfull lot of test code for such a trivial 'About' method.
Requesting Gravatar... Haacked Dec 10, 2007 9:48 AM
# re: Writing Unit Tests For Controller Actions
@Bill Barry - Exactly! I like your pragmatism.

@Peli - It is. After all, TDD is about producing a better design. We now have a test we can use as a focal point for improving our design.
Requesting Gravatar... Aaron Jensen Dec 10, 2007 9:55 AM
# re: Writing Unit Tests For Controller Actions

If you attempt to mock these objects, you sometimes also have to mock their dependencies and their dependencies' dependencies and so on, depending on what you are trying to test.


Could you explain this? Once you mock something out their dependencies are irrelevant.
Requesting Gravatar... Haacked Dec 10, 2007 11:14 AM
# re: Writing Unit Tests For Controller Actions
Method Foo takes in an IHttpContext. So I dutifully mock IHttpContext. However, Foo wants to call a method on HttpContext.Request. Now I need to mock out IHttpRequest so that my IHttpContext mock return an instance of IHttpRequest to my method Foo.

It's good to try to keep call trains short (aka avoid Foo.Bar.Baz.Quux), but even short ones require you mock each point in the chain.
Requesting Gravatar... Ben Hall Dec 10, 2007 5:02 PM
# re: Writing Unit Tests For Controller Actions
Don't know if I'm missing something or not, but I can't seem to get VerifyIndexSelectsCorrectViewUsingMockViewFactory() to pass.

I also couldn't find it in your download code. What am I missing?

Exception:
Error 1 TestCase 'HomeControllerTests.VerifyIndexSelectsCorrectViewUsingMockViewFactory'
failed: Value cannot be null.
Parameter name: tempData
System.ArgumentNullException
Message: Value cannot be null.
Parameter name: tempData
Source: System.Web.Extensions
StackTrace:
at System.Web.Mvc.ViewContext..ctor(IHttpContext httpContext, RouteData routeData, IController controller, Object viewData, TempDataDictionary tempData)
Requesting Gravatar... Haacked Dec 10, 2007 5:42 PM
# re: Writing Unit Tests For Controller Actions
Hmmm... Let me double check. I recently re-uploaded the code when I added the MbUnit.
Requesting Gravatar... Haacked Dec 10, 2007 6:19 PM
# re: Writing Unit Tests For Controller Actions
Shoot, my test breaks because I wrote that before the final final CTP. Something must've changed. I'll fix it soon.
Requesting Gravatar... Scott Hanselman's Computer Zen Dec 11, 2007 11:26 PM
# ASP.NET 3.5 Extensions - plus MVC How-To Screencast
Requesting Gravatar... Scott Bellware Dec 12, 2007 9:07 PM
# re: Writing Unit Tests For Controller Actions
> Method Foo takes in an IHttpContext. So I
> dutifully mock IHttpContext. However, Foo
> wants to call a method on HttpContext.Request.
> Now I need to mock out IHttpRequest so that
> my IHttpContext mock return an instance of
> IHttpRequest to my method Foo.

That looks a bit like a dependency hub antipattern. Aaron was saying that once you mock something, its dependencies aren't needed. Applying this to the HttpContext.Request problem would suggest that either Foo() should take an IHttpRequest, or that IHttpContext should expose an API that does the stuff that you'd use IHttpRequest for (http://en.wikipedia.org/wiki/Law_of_Demeter).
Requesting Gravatar... Haacked Dec 12, 2007 11:09 PM
# re: Writing Unit Tests For Controller Actions
Right. The trade-off here is that Foo might need to access the Request property. It might need the Response property. We don't know because Foo is a virtual, so we want to make these things available to implementers.

We could change the interface to pass in all the properties of HttpContext we *think* people might need. But that could bulk up the call and we might miss one.

I agree with the Law of Demeter, I just think it's a tough law to follow in all cases, especially when you're building an extensibility point since you don't know what crazy ideas someone will come up with.

Again, to summarize. I still think testing with subclass is fine. In this specific case, we do have plans to make testing easier using mocks via some refactorings to improve the design of the code. I'd like the design to be such that we can follow the law of demeter as much as feasible.
Requesting Gravatar... Byron Dec 15, 2007 3:34 AM
# re: Writing Unit Tests For Controller Actions
Hi Phil,

Im having difficulty testing the controllers actions by mocking everything.

The ControllerContext class has no interface so I can't mock it. Id like to expect a call to ViewContext.GetControllerContext, but ViewContext, ControllerContext or RequestContext dont have interfaces.

Am I doing something wrong or should I request you guys t put interfaces on these classes?
Requesting Gravatar... Haacked Dec 15, 2007 10:41 PM
# re: Writing Unit Tests For Controller Actions
Byron, just instantiate those classes. No need to mock them.
Requesting Gravatar... Trumpi's blog Dec 16, 2007 10:11 AM
# Our daily link (2007-12-11)
I&amp;#39;m replacing my del.icio.us feed with posts to this blog. That way they will appear on the website
Requesting Gravatar... David Hayden [MVP C#] Dec 16, 2007 3:30 PM
# Model-View-Presenter Seems Easier to Test Than ASP.NET MVC
I have been creating an application using the new ASP.NET MVC Framework in the ASP.NET 3.5 Extensions
Requesting Gravatar... Haacked Dec 17, 2007 1:07 AM
# re: Writing Unit Tests For Controller Actions
I've rewritten some of the tests and uploaded the solution with my latest post.
Requesting Gravatar... Rubinator Dec 23, 2007 7:57 AM
# Writing Unit Tests For Controller Actions
I've tried to reproduce some of your Unit Tests. Especially this one.
public void VerifyIndexSelectsCorrectViewUsingTestSpecificSubclass()
{
HomeControllerTester controller = new HomeControllerTester();
controller.Index();
Assert.AreEqual("Index", controller.SelectedViewName
, "Should have selected 'Index'.");
}

This works fine as long as I don`t instantiate a DataContext out of my Modell the test fails with an NullReferenceexception.
Is this behaviour useful and if so how can i mock my DataContext objects?
Requesting Gravatar... Alexey Dec 24, 2007 6:00 AM
# re: Writing Unit Tests For Controller Actions
Hi Phil,
I have the same problem as the one that Ben Hall has (VerifyIndexSelectsCorrectViewUsingMockViewFactory fails): same exception, same stack trace. Reflector shows me that the only place where TempData setter is called is Controller.Execute method, which, in its turn, is called by MvcHandler.ProcessRequest method but none of the methods used in your test.
The test passes when I insert the following line:
controller.GetType().GetProperty("TempData").SetValue(controller, new TempDataDictionary(httpContextMock), null);
But sorry, I can't stand it.
Am I still going the wrong way?
Thanks.
Requesting Gravatar... Haacked Dec 24, 2007 3:31 PM
# re: Writing Unit Tests For Controller Actions
@alexey we'll improve this in the next release. I think what you've done is the only way for the CTP so far.
Requesting Gravatar... Alexey Dec 25, 2007 11:35 PM
# re: Writing Unit Tests For Controller Actions
Hi Phil,
Thank you for the answer – can’t wait to play with release version :)
There is another suggestion I’ve got as a result of my experiments.
IMHO, using mocked IViewFactory (like it’s done in your samples) is not very convenient,
though it’s absolutely fine for me to use mocked IHttpContext (as it’s only a single line of code).
The problem with mocked view is that complexity of mocking code increases as you need to get
more complex behavior from view. As a result, you get barely readable code (the code which
creates mocked objects).
For the views, I prefer using a simple fake class which implements 3 interfaces – everything you
need to mock view behavior:

internal class MockView : IViewFactory, IView, IViewDataContainer {
private object viewData = null;
private string viewName = null;
#region IViewFactory Members
public IView CreateView(ControllerContext controllerContext, string viewName, string masterName, object viewData) {
this.viewName = viewName;
this.viewData = viewData;
return this;
}
#endregion
#region IView Members
public void RenderView(ViewContext viewContext) {
return;
}
#endregion
#region IViewDataContainer Members
public object ViewData {
get {
return viewData;
}
}
#endregion
public object ViewName {
get {
return viewName;
}
}
}

In this case, tests become more readable e.g.

[TestMethod]
public void ProductCategoriesUsesCorrectView() {
ProductsController controller = new ProductsController();
MockView mockView = new MockView();
MockRepository mocks = MvcHelpers.InitializeControllerWithMockedData(controller, mockView);
controller.Categories();
mocks.VerifyAll();
Assert.AreEqual("Categories", mockView.ViewName);
}

Note that here, in case of using incorrect view in controller code, the test would fail with more readable error message (because AreEqual call fails) whereas in original code (with mocks) that would be System.NullReferenceException which is not so obvious.
Also, with fake class, you can easily check whether data passed to view correct or not:

[TestMethod]
public void ProductCategoriesHasData() {
ProductsController controller = new ProductsController();
MockView mockView = new MockView();
MockRepository mocks = MvcHelpers.InitializeControllerWithMockedData(controller, mockView);

controller.Categories();

mocks.VerifyAll();
Assert.IsInstanceOfType(mockView.ViewData, typeof(List<ProductCategory>));
List<ProductCategory> categories = mockView.ViewData as List<ProductCategory>;
Assert.AreNotEqual(0, categories.Count);
}

I tried to implement that behavior using mocks, but, as I said at the beginning, I got almost non-readable code which dealt with creating of mocked objects.
Again, everything here is my IMHO.
And, of course, thanks a lot for the job your team is doing in ASP.NET MVC – it’s really cool framework and I like it!
Requesting Gravatar... Kyle Baley - The Coding Hillbilly Jan 10, 2008 5:53 AM
# AutoMockingContainer and ASP.NET MVC: Round 1
I resolved to pace myself a little better in &amp;#39;08 on this blog. But then, I also resolved not to keep
Requesting Gravatar... Kyle Baley - The Coding Hillbilly Jan 25, 2008 9:44 AM
# West Palm Beach and the Fluent URLs
Ah, it&amp;#39;s good to be back on the circuit. The Hillbilly made his first speaking engagement since May
Requesting Gravatar... My Blog On .NET Feb 04, 2008 2:08 PM
# ASP.NET MVC Framework Links
ASP.NET MVC Framework Links
Requesting Gravatar... Something Mar 30, 2008 9:07 PM
# re: Writing Unit Tests For Controller Actions
@Alexy

Try TypeMock.com
Requesting Gravatar... Nolan Apr 09, 2008 10:55 PM
# re: Writing Unit Tests For Controller Actions
Typically if you need to mock several layers of dependencies to test one class it is because the law of demeter has been heavily violated. Some schools of thoughts may try to tell you this isn't such a bad thing, however other schools would disagree. I personally think that smelly tests are a sign of smelly code, and that inheriting each and every controller to test it is very smelly.

Jay Fields has a good post on the topic of law of demeter violations and testability:
http://blog.jayfields.com/2007/05/law-of-demeter-canary-in-coal-mine.html

What do you have to say?

(will show your gravatar)
Please add 3 and 8 and type the answer here: