Writing Unit Tests For Controller Actions

asp.net, code, asp.net mvc, tdd 0 comments suggest edit

UPDATE: Completely ignore the contents of this post. All of this is out-dated. Test specific subclasses are no longer necessary with ASP.NET MVC since our April CodePlex refresh

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: aspnetmvc , TDD

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

30 responses

  1. Avatar for ASPInsiders
    ASPInsiders December 9th, 2007

    Darn that ScottGu, he's scooped me again . Just kidding. Around dinner time this evening we released

  2. Avatar for ScottGu's Blog
    ScottGu's Blog December 9th, 2007

    Earlier today we released the first CTP preview of an &amp;quot;ASP.NET 3.5 Extensions&amp;quot; release that

  3. Avatar for Aaron Jensen
    Aaron Jensen December 9th, 2007

    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 :)

  4. Avatar for Bill Barry
    Bill Barry December 9th, 2007

    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.

  5. Avatar for Steven Harman
    Steven Harman December 9th, 2007
    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? :)

  6. Avatar for Peli
    Peli December 9th, 2007

    Looks like an awfull lot of test code for such a trivial 'About' method.

  7. Avatar for Haacked
    Haacked December 9th, 2007

    @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.

  8. Avatar for Aaron Jensen
    Aaron Jensen December 9th, 2007

    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.

  9. Avatar for Haacked
    Haacked December 10th, 2007

    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.

  10. Avatar for Ben Hall
    Ben Hall December 10th, 2007

    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:
    Error1TestCase '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)

  11. Avatar for Haacked
    Haacked December 10th, 2007

    Hmmm... Let me double check. I recently re-uploaded the code when I added the MbUnit.

  12. Avatar for Haacked
    Haacked December 10th, 2007

    Shoot, my test breaks because I wrote that before the final final CTP. Something must've changed. I'll fix it soon.

  13. Avatar for Scott Bellware
    Scott Bellware December 12th, 2007

    > 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/wik....

  14. Avatar for Haacked
    Haacked December 12th, 2007

    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.

  15. Avatar for Byron
    Byron December 14th, 2007

    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?

  16. Avatar for Haacked
    Haacked December 15th, 2007

    Byron, just instantiate those classes. No need to mock them.

  17. Avatar for Trumpi's blog
    Trumpi's blog December 15th, 2007

    I&amp;#39;m replacing my del.icio.us feed with posts to this blog. That way they will appear on the website

  18. Avatar for David Hayden [MVP C#]
    David Hayden [MVP C#] December 16th, 2007

    I have been creating an application using the new ASP.NET MVC Framework in the ASP.NET 3.5 Extensions

  19. Avatar for Haacked
    Haacked December 16th, 2007

    I've rewritten some of the tests and uploaded the solution with my latest post.

  20. Avatar for Rubinator
    Rubinator December 22nd, 2007

    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?

  21. Avatar for Alexey
    Alexey December 23rd, 2007

    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.

  22. Avatar for Haacked
    Haacked December 24th, 2007

    @alexey we'll improve this in the next release. I think what you've done is the only way for the CTP so far.

  23. Avatar for Alexey
    Alexey December 25th, 2007

    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!

  24. Avatar for Kyle Baley - The Coding Hillbi
    Kyle Baley - The Coding Hillbi January 9th, 2008

    I resolved to pace myself a little better in &amp;#39;08 on this blog. But then, I also resolved not to keep

  25. Avatar for Kyle Baley - The Coding Hillbi
    Kyle Baley - The Coding Hillbi January 24th, 2008

    Ah, it&amp;#39;s good to be back on the circuit. The Hillbilly made his first speaking engagement since May

  26. Avatar for My Blog On .NET
    My Blog On .NET February 4th, 2008

    ASP.NET MVC Framework Links

  27. Avatar for Something
    Something March 30th, 2008

    @Alexy
    Try TypeMock.com

  28. Avatar for Nolan
    Nolan April 9th, 2008

    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/2...

  29. Avatar for James Radford
    James Radford May 9th, 2010

    Hi, thanks for the post, even though its out-dated.. Not sure if I'll get a response but I'll try anyway.. does anyone have any suggestions how to test a controller that uses the Request object?
    Thanks!

  30. Avatar for pavankumar
    pavankumar May 9th, 2011

    When Authentication is present in the site how do we handle this .