MVC: Unit testing controller actions that use TempData

Part of the "big deal" about ASP.NET MVC, and of course MVC in general, is that the code you write is more easily unit tested as compared to traditional ASP.NET WebForms. However, if you've used ASP.NET MVC and tried to write a unit test for a controller action that uses TempData, you probably got some exceptions about null references and other crazy stuff. I've seen a number of solutions out there that involve private reflection (which is kind of evil) so I figured I'd show how it can be done without that trickery. (Kudos, though, to those of you who figured out how to do such trickery!)

The underlying problem is that the TempDataDictionary object tries to read from session state when it's first used. At unit test time ASP.NET is not running so there isn't any session state. The solution is to provide a mock session state object to the TempDataDictionary. We're basically telling TempDataDictionary, "Hey you want some session state??? Here's your session state!!!"

The controller we're going to test has two actions. The first action adds a value to TempData and then does a redirect. The second action checks for the existence of the value in TempData and renders a view.

public class HomeController : Controller {
    public void Index() {
        // Save UserID into TempData and redirect to greeting page
        TempData["UserID"] = "user123";
        RedirectToAction("GreetUser");
    }

    public void GreetUser() {
        // Check that the UserID is present. If it's not
        // there, redirect to error page. If it is, show
        // the greet user view.
        if (!TempData.ContainsKey("UserID")) {
            RedirectToAction("ErrorPage");
            return;
        }
        ViewData["NewUserID"] = TempData["UserID"];
        RenderView("GreetUser");
    }
}

In order to write the unit tests I created a mock HttpContext with a mock session state object:

// HttpContext for TempData that uses a custom
// session object.
public class TestTempDataHttpContext : HttpContextBase {
    private TestTempDataHttpSessionState _sessionState =
        new TestTempDataHttpSessionState();

    public override HttpSessionStateBase Session {
        get {
            return _sessionState;
        }
    }
}

// HttpSessionState for TempData that uses a custom
// session object.
public class TestTempDataHttpSessionState : HttpSessionStateBase {
    // This string is "borrowed" from the ASP.NET MVC source code
    private string TempDataSessionStateKey = "__ControllerTempData";
    private object _tempDataObject;

    public override object this[string name] {
        get {
            Assert.AreEqual<string>(
                TempDataSessionStateKey,
                name,
                "Wrong session key used");
            return _tempDataObject;
        }
        set {
            Assert.AreEqual<string>(
                TempDataSessionStateKey,
                name,
                "Wrong session key used");
            _tempDataObject = value;
        }
    }
}
Each unit test that involves an action that uses TempData needs to create a custom TestTempDataHttpContext object and use that in a new instance of TempDataDictionary:
TestTempDataHttpContext tempDataHttpContext = new TestTempDataHttpContext();
homeController.TempData = new TempDataDictionary(tempDataHttpContext);

I wrote three unit tests for HomeController:

  1. Test that the Index action sets the right TempData and does a redirect
  2. Test that the GreetUser action redirects when the TempData is missing the value
  3. Test that the GreetUser action renders the GreetUser view with the right ViewData when the TempData has the right value

And here are the unit tests:

[TestClass]
public class HomeControllerTest {
    [TestMethod]
    public void IndexSavesUserIDToTempDataAndRedirects() {
        // Setup
        TestHomeController homeController = new TestHomeController();
        TestTempDataHttpContext tempDataHttpContext = new TestTempDataHttpContext();
        homeController.TempData = new TempDataDictionary(tempDataHttpContext);

        // Execute
        homeController.Index();

        // Verify
        Assert.IsTrue(homeController.RedirectValues.ContainsKey("action"));
        Assert.AreEqual("GreetUser", homeController.RedirectValues["action"]);

        Assert.IsTrue(homeController.TempData.ContainsKey("UserID"));
        Assert.AreEqual("user123", homeController.TempData["UserID"]);
    }

    [TestMethod]
    public void GreetUserWithNoUserIDRedirects() {
        // Setup
        TestHomeController homeController = new TestHomeController();
        TestTempDataHttpContext tempDataHttpContext = new TestTempDataHttpContext();
        homeController.TempData = new TempDataDictionary(tempDataHttpContext);

        // Execute
        homeController.GreetUser();

        // Verify
        Assert.IsTrue(homeController.RedirectValues.ContainsKey("action"));
        Assert.AreEqual("ErrorPage", homeController.RedirectValues["action"]);

        Assert.AreEqual(0, homeController.TempData.Count);
    }

    [TestMethod]
    public void GreetUserWithUserIDCopiesToViewDataAndRenders() {
        // Setup
        TestHomeController homeController = new TestHomeController();
        TestTempDataHttpContext tempDataHttpContext = new TestTempDataHttpContext();
        homeController.TempData = new TempDataDictionary(tempDataHttpContext);
        homeController.TempData["UserID"] = "TestUserID";

        // Execute
        homeController.GreetUser();

        // Verify
        Assert.AreEqual<string>("GreetUser", homeController.RenderViewName);
        Assert.AreEqual<string>(String.Empty, homeController.RenderMasterName);
        IDictionary<string, object> viewData =
            homeController.RenderViewData as IDictionary<string, object>;
        Assert.IsNotNull(viewData);
        Assert.IsTrue(viewData.ContainsKey("NewUserID"));
        Assert.AreEqual("TestUserID", viewData["NewUserID"]);
    }

    // Test-specific subclass for HomeController. This won't be
    // needed in the next release of ASP.NET MVC.
    private sealed class TestHomeController : HomeController {
        public RouteValueDictionary RedirectValues;
        public string RenderViewName;
        public string RenderMasterName;
        public object RenderViewData;

        protected override void RedirectToAction(RouteValueDictionary values) {
            RedirectValues = values;
        }

        protected override void RenderView(string viewName, string masterName,
            object viewData) {
            RenderViewName = viewName;
            RenderMasterName = masterName;
            RenderViewData = viewData;
        }
    }
}

Some notes about the code:

  • For the unit tests I also needed to create a test-specific subclass for HomeController called TestHomeController in order to capture the values of a call to RedirectToAction and RenderView. In the upcoming preview of ASP.NET MVC this class won't be needed due to some changes we've made.
  • Making TempData easier to unit test is on our list of improvements for a later preview of ASP.NET MVC. We realize it's a bit tricky right now, and we plan to make it much, much easier.
  • Instead of creating concrete classes for the custom HttpContext and session state objects you could use a mock object framework to dynamically generate those instances. I chose not to show that since it might detract from the purpose of the post. If you're already using a mock object framework such as RhinoMocks or Moq it should be easy to modify the code to use those frameworks.

I hope you find this sample useful when you're writing your unit tests. If you have any other pain points in ASP.NET MVC please post a comment so we'll know about it.

8 Comments

  • This is exactly what we're doing in CodeCampServer, however it's quite obvious that there is pain in the manual testing classes.

    Even with mocks, the related code is less, but the intent of the test can easily get lost in the noise of the mocking-fu.

    I'm glad to hear that you guys are working on the testing scenario, and I expect the next drop to reflect this ;)

    I'm curious though (and I mentioned this to you in person) why isn't TempData just an IDictionary with a public setter?

    Anyway, keep us updated with changes as you go along.

  • Hi Ben,
    TempData is its own type since it has to do change tracking while it's being used. Since in a given request you can both read TempData from the previous request as well as write data to be used in the next request, we have to differentiate between what's new and what's old. A regular dictionary doesn't do that tracking and you'd end up with an ever-growing dictionary of TempData :)

    - Eilon

  • I'm talking about the class declaration. Controller would instantiate it's TempDataDictionary, but ultimately we would just interface with an IDictionary.

    this way in testing I can just stub it out with new Dictionary(); and it would work nicely.

    At runtime the stock one would be used.

  • Hi Ben,
    If the class declaration itself is just of type IDictionary then other people using it can't make assumptions about its storage mechanism and call its storage methods (e.g. SaveToStorage and LoadFromStorage). While we haven't finalized the new design, the idea would be that you could just instantiate an empty TempDataDictionary and as long as you're just doing normal read/writes to it, it won't try to use session state or any other quasi-persistent storage. At runtime the Controller class (or something else) will hook it up to session state and do the real storage - but none of that would execute at test time.

    Thanks,
    Eilon

  • I'm not really sure I see the purpose of TempData. When using Monorail they also had the concept of flash. I kind of see these as a solution in search of a problem. They smell to me as breaking the stateless nature of the web and I've yet to see a good reason to use them over just passing the data around on the request. I'm worried this may end up being overly used like Session is. On every asp.net application I have ever jumped into Session state is so heavily abused it is scary.

  • This post is now out-dated due to MVC Preview 4 which makes testing TempData much easier.

  • i found this article while trying to cover my MVC.Net application. It is quite decent, since it covers all the layers of the MVC framework and even contains DB layer testing. The related code can be helpful to anyone who is going to unit test MVC.

  • Did this, "Making TempData easier to unit test is on our list of improvements" make it into MVC3? Cannot figure out how to test a custom Action Filter that uses TempData.

Comments have been disabled for this content.