Testing TempData in ASP.NET MVC

Eventually when testing your controllers you will come across an action that sets TempData.

If you didn’t know, TempData is a session-backed temporary storage dictionary that is available for one single request.  It’s great to pass messages between controllers.

Our Controller classes that we derive from have this as a member property.  However, when testing the controller, this always came up as null.

Reflector is our friend here, so I fired it up and loaded the Controller class.  Then I located the TempData member, and looked at the setter.  It’s marked as internal, so somebody inside this assembly is responsible for creating it.  By using the Reflector Analyze feature, I was able to find exactly where the setter is called.  It’s in the Execute() method.  (this is the general “pipeline” method that all IController implementations must override)

Think about that for a second.  At runtime, the TempData dictionary is set because all requests flow through Execute().  But in my test, I am specifically calling my action method.  There is no Execute() in my test.

To top it off, I can’t set this guy to a new TempDataDictionary because the setter is marked as internal.

Phil Haack tells me they’re working on this issue so we’ll likely see an improvement soon.  In the meantime you can get around the issue by using reflection to set the value.  But first, can we create one?

TempData is stored in session, so it needs a reference to IHttpContext.  Thankfully, we can mock this, however we have to make sure and mock the right parts.

First, create a mock for IHttpContext, create the return values for mocked IHttpRequest & IHttpResponse.  Finally setup return values for IHttpSession as well.  I’ve done all of this in a helper method of my test (though it could be an extention method on the MockRepository ala Phil, but whatever you like).

private IHttpContext GetHttpContext(string requestUrl){	IHttpRequest request = _mocks.DynamicMock<IHttpRequest>();	SetupResult.For(request.Url).Return(new Uri(requestUrl));	IHttpResponse response = _mocks.DynamicMock<IHttpResponse>();	IHttpSessionState session = _mocks.DynamicMock<IHttpSessionState>();IHttpContext httpContext = _mocks.DynamicMock<IHttpContext>();	SetupResult.For(httpContext.Session).Return(session);	SetupResult.For(httpContext.Request).Return(request);	SetupResult.For(httpContext.Response).Return(response);_mocks.Replay(request);_mocks.Replay(response);_mocks.Replay(httpContext);

_tempData = new TempDataDictionary(httpContext);
SetupResult.For(session[null]).IgnoreArguments().Return(_tempData);
_mocks.Replay(session);

 return httpContext;}

This code is a little verbose, but this is something you write once and use in all of your tests. You should notice that the _tempData property is a local field on my test class. I can then take this _tempData variable and inject it (via reflection) onto the controller under test. Here's how:

typeof(FooController).GetProperty("TempData").SetValue(controller, _tempData, null);

And now we can easily test that messages are set in TempData before redirecting or rendering a view.

*update*:  Will Shaver notified me of a typo in the code.  It has been fixed.  Thanks Will!*

#1 Will Shaver avatar
Will Shaver
1.31.2008
5:44 PM

Yet another instance of internal methods being evil. Ever since we've been able to bust open the code with Reflector and see them, internal methods have been the bane of my asp.net experience. (A close second to viewstate, but we're trying to get away from that now.) Internal methods are sitting there, teasing us with the exact code that we need to modify to accomplish our task... I feel like Tantalus.


#2 Justice~! avatar
Justice~!
2.01.2008
8:26 AM

I had a comment, but it turned into its own post...http://graysmatter.codivation.com/MockingRequestFormAndTempDataInTheASPNETMVC.aspx


#3 Will Shaver avatar
Will Shaver
2.06.2008
5:31 PM

Two things that hung me up:1) _tempData = new TempDataDictionary(httpContext);This line needs to be AFTER your _mocks.Replay(context) line.2) SetupResult.For(session[null]).IgnoreArguments().Return(tempData); should be _tempDataThanks for the help though...