Sponsored By Aspose - File Format APIs for .NET

Aspose are the market leader of .NET APIs for file business formats – natively work with DOCX, XLSX, PPT, PDF, MSG, MPP, images formats and many more!

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:

IList source = 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

This entry was posted in Uncategorized. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Renso

    using (_mocks.Playback())
    {
    RedirectToRouteResult result = _controller.Show(1) as RedirectToRouteResult;
    Assert.AreEqual(“New”, result.Values[“action”]);

  • Kyle Baley

    @ScMoMo: Don’t know if I understood your question but that won’t stop me answering based on assumptions. The View can get the URI but there should be no need to create actual views to test controllers. And even if you did, RedirectToAction doesn’t return the View that it redirected to.

  • ScMoMo

    Is it possible for a View (in its codebehind) to know what its URI is and what each part is (controller/action/param/param/param)? That might aid in “runtime testing” as well.

  • http://codeclimber.net.nz Simone

    Klye,
    I’m writing an article on this topic for DotNetSlacker, but given the situation I think I can post a spoiler here :)

    Here is a test method that tests the RedirectToAction. It’s not very different from the one Phil wrote on the first preview of MVC (actually I think it’s the same :))

    [TestMethod]
    public void AboutMock()
    {
    RouteTable.Routes.Clear();
    RouteTable.Routes.Add(new Route(“{controller}/{action}”, new MvcRouteHandler()));

    HomeController controller = new HomeController();

    MockRepository mocks = new MockRepository();
    ControllerContext context;
    using (mocks.Record())
    {
    var httpContext = mocks.FakeHttpContext();

    //Setting up the return value for the ApplicationPath property
    SetupResult.For(httpContext.Request.ApplicationPath).Return(“/”);

    //Expect a call to the Redirect method with that parameter
    httpContext.Response.Redirect(“/Home/Index”);

    RouteData routeData = new RouteData();
    routeData.Values.Add(“Controller”, “Home”);
    context = new ControllerContext(new RequestContext(httpContext, routeData), controller);
    }

    using (mocks.Playback())
    {
    controller.ControllerContext = context;
    controller.About();
    }
    }

    The RouteData is populated because the RedirectToAction called only with the name of the action looks at the current Controller name in order to call the right URL. And the name of the controller is in the RouteData.
    I guess that if I was using the override of the RedirectToAction method that accepts also the Controller name, the RouteData would not be needed.
    Hope this help (or just contact me and we can discuss about it)

  • http://mikehadlow.blogspot.com Mike Hadlow

    Hi Kyle, great blog!

    I’m using Ayende’s Partial Mock and Extension method pattern for testing RedirectToAction. You have an extension method on Controller that ‘makes RedirectToAction public':

    public static void RedirectToAction(this Controller controller, object values)
    {
    typeof(Controller).GetMethod(“RedirectToAction”,
    System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
    null,
    new Type[] { typeof(RouteValueDictionary) },
    null)
    .Invoke(controller, new object[] { values });
    }

    The good thing is you only have to write this once rather than having a test subclass for every controller. Then in your tests you partial mock (I’m using Rhino Mocks) your controller:

    loginController = mocks.PartialMock();

    And then when you set up your expectations you can use a callback to check what’s passed to RedirectToAction.

    RouteValueDictionary routeValues = null;
    loginController.RedirectToAction(null);
    LastCall.Callback(new Func(rvd => { routeValues = rvd; return true; }));

    It works, but yes, it’s a shame to have to do all this hackery.

  • Kyle Baley

    @Simone

    Yeah, the “Extract and Override Call” looks like the Test-Specific Subclass from Phil’s other post on the subject. That’s specifically what I was trying to move away from. I missed the part where he populated the RouteData variable before creating the ControllerContext in Phil’s earlier post. That, I think, is why this method isn’t working with the MvcMockHelpers. The context is created with an empty RouteData but in the actual application, the framework sets it up somewhere.

  • http://codeclimber.net.nz Simone

    Kyle, I played around a bit with it as well.
    But I came out that at the moment the only way to do it is either to use the super mocking sample written by Phil in his post of december, or using the “Extract and Override Call” testing pattern to change the way the RedirectToAction behave.
    I wrote some idea a few days ago about this:
    http://codeclimber.net.nz/archive/2008/03/10/Playing-with-testing-ASP.NET-MVC-P2.aspx