Testing RedirectToAction in ASP.NET MVC
Crikey, am I having a time testing RedirectToAction on my controller with the new MVC Preview! How much of it is me and how much is the beta-ness of the framework, I will leave to your fair and impartial judgement.
I upgraded relatively easily from the December CTP. Here's a quick summary of what was involved:
- Change the assembly references from System.Web.Extensions to the three new ones that are installed with the new preview (and I'd *really* like to know why that install takes so long if all it does is drop some files, add some shortcuts, and install a few VS templates. Might be time to upgrade the laptop.)
- Drop the reference to MvcToolkit
- Update the 80s-style square brackets to the new age curly braces. Kind of like the evolution of the Volvo.
- Update the method used to add routes. Thanks to newly-required constructor arguments and the funky new RouteValueDictionary, they now look like this:
RouteTable.Routes.Add( new Route( "{controller}.mvc/{action}/{id}", new MvcRouteHandler( ) ) { Defaults = new RouteValueDictionary( new { action = "Index", id = (string) null } ) } );
- Update my call to RedirectToAction, again to use the RouteValueDictionary (which, judging from Reflector, we can only assume is a class-in-progress).
- Update the web.config as I was told to.
- Dropping the Html. from the beginning of all my ResolveUrl calls in the views.
- Updated calls to various other methods that used to be part of MvcToolkit.
I think that was about it. After that, the bad boy compiled and all the tests passed and it was on to my next challenge. Namely, drop the test-specific subclasses in favour of the extension methods mentioned by Haackselman in post and video form.
It went pretty smoothly at first. I added the MvcMockHelpers class, incorporated a FakeViewEngine, and went about my merry way converting asserts that used testController.RenderedView to ones using fakeViewEngine.ViewContext.ViewName. Ditto for RenderedViewData to ViewData.
Then came the last test. The one that checked to see if the action correctly called RedirectToAction. There is no property in the ViewContext to check to see which view was the eventual target. And after checking out the only post I could find on the subject (which has a lot more information on it today than it did two days ago when I went through this problem), I got an error in the test that turned out to be misleading:
System.NullReferenceException: Object reference not set to an instance of an object.
at Trilogy.Gunton.Web.MyRoute.GetRouteData(HttpContextBase httpContext) in MyRoute.cs: line 117
at System.Web.Routing.RouteCollection.GetRouteData(HttpContextBase context)
at Trilogy.Gunton.Tests.Unit.Controllers.JobControllerFixture.Save_action_should_send_job_to_service() in JobControllerFixture.cs: line 150
The MyRoute class is courtesy of Reflector and I used it to determine the problem. It was two days ago and I don't have the memory I used to but I'm pretty sure it was something deep within the bowels of the Route class. It's calling GetRouteData on the class to determine which URL to redirect to and that method has a call:
IListsource = SplitUrl(httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo);
And the AppRelativeCurrentHolyCowThisIsALongName and PathInfo properties have not been mocked. I think it has something to do with the fact that the controller's fake ControllerContext is created with an empty RouteData. Not sure how the RouteTable created in Global.asax.cs eventually translates into the RouteData (assuming it does) because by that time, I had enough fodder for this post (which is the only reason I took it as far as I did).
Again, this is speculation but the net result is, RedirectToAction remains untestable by me. I've heard tell that there is a way but as it stands now, I've put some comments into my tests to remind me to revisit them once a better way exists.
But if you're the type that's more anal about code coverage than best practices, using Response.Redirect instead of RedirectToAction works like a charm.
Kyle the Undirected