Geeks With Blogs
// ThomasWeller C#/.NET software development, software integrity, life as a freelancer, and all the rest

I love Test Driven Development. I use this development approach wherever I can. It makes my code a lot better  and gives me the confidence that I indeed have crafted a good and reliable piece of code. However, some things are quite hard to test-drive, simply because they have so many dependencies that you would need to make extensive use of a mocking framework (and probably inspecting some things via Reflector...). In such a case, you usually decide to not have unit tests, because the time and amount of code needed for testing would be in a very disadvantageous relation to the tested code itself. In short: It wouldn't pay. One such case is the routing table of an ASP.NET MVC application...

The problem: Routes are hard to test

While the ASP.NET MVC framework generally does allow for easily test-driving almost everything because of its highly component-oriented structure, testing routes is not very easy or intuitive. How would you for example test the following route mechanism (it's the default that is generated for a new ASP.NET MVC project):

public class MvcApplication : System.Web.HttpApplication

{

    public static void RegisterRoutes(RouteCollection routes)

    {

        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

 

        routes.MapRoute(

            "Default",                                              // Route name

            "{controller}/{action}/{id}",                           // URL with parameters

            new { controller = "Home", action = "Index", id = ""// Parameter defaults

        );

    }

 

    ...

Well, the 'traditional', hard way to test such a route would be  to isolate the required component with mocks. Using Rhino.Mocks, your unit test for a single URL might finally look something like this:

[Test]

[Description("Verifies that the URL root (empty) matches the 'HomeController.Index()' action.")]

public void TestRootUrlMatchesHomeIndex()

{

    // Arrange

    const string url = "~/";

    var mockRequest = MockRepository.GenerateStub<HttpRequestBase>();

    var mockContext = MockRepository.GenerateStub<HttpContextBase>();

 

    mockRequest.Stub(r => r.AppRelativeCurrentExecutionFilePath)

               .Return(url)

               .Repeat.Any();

    mockRequest.Stub(r => r.PathInfo)

               .Return(string.Empty)

               .Repeat.Any();

    mockContext.Stub(c => c.Request)

               .Return(mockRequest)

               .Repeat.Any();

 

    // Act

    RouteTable.Routes.Clear();

    MvcApplication.RegisterRoutes(RouteTable.Routes);

    var routeData = RouteTable.Routes.GetRouteData(mockContext);

 

    // Assert

    Assert.Multiple(() =>

    {

        Assert.AreEqual("home",

                        routeData.Values["controller"].ToString(),

                        StringComparison.InvariantCultureIgnoreCase);

        Assert.AreEqual("index",

                        routeData.Values["action"].ToString(),

                        StringComparison.InvariantCultureIgnoreCase);

    });

}

Needless to say that no one will ever write such a test (except for demonstration purposes...) - it's far too time-consuming and complicated, moreover it is very hard to read (or could you tell from first glance what this test is doing?). And remember: You have to write such a test for every single possible URL, and even a slightly complex application has dozens of possible routes...

Apart from that, the above unit test will produce this entry in the test report, when running it through the Gallio test driver:

 

Not that much information about what's going on, huh? If there wasn't a Description attribute added to the code, the test report would give no hint about what's going on other than the method's name. Hopefully, the author has chosen it wisely...

The solution: MVC Contrib and a self-written fixture base class

This is where the excellent MVC Contrib OS project comes to the rescue. Its libraries add additional functionality on top of the ASP.NET MVC framework which are aimed to make it easier to use, thus increasing a developer's productivity with the framework. Some of the provided goodies are:

  • ViewDataExtensions to provide strongly-typed viewdata for multiple objects
  • IoC container controller factories e.g. for StructureMap, Windsor, Spring.Net, etc
  • Extra view helpers
  • Generic test doubles for unit testing
  • Visual Studio - Code Snippets
  • ReSharper Live Templates

You may check out a more detailed overview here.

One part of the project is the MvcContrib.TestHelper assembly, which contains some useful stuff to help in unit testing different parts of the MVC framework. This is what I will use in the following, especially the RouteTestingExtensions class therein, which does the above shown mocking magic for us and allows us to rewrite the above test like this: 

[Test]

[Description("Verifies that the URL root (empty) matches the 'HomeController.Index()' action.")]

public void TestRootUrlMatchesHomeIndexUsingMvcContrib()

{

    "~/".ShouldMapTo<HomeController>(controller => controller.Index());

}

 Much better yet... Our test method has become a simple, easy-to-understand one-liner; and it clearly communicates, what the code is doing. But two not-so-nice things remain:

  • The test report doesn't give much information about the test.
  • You have to write an extra test for every single URL.

Therefore, I decided to invest some hours in writing my own base class for test fixtures that target MVC routes. Basically, it allows to put an application's routing scheme under test with tabular-style definitions, using data-driven unit test methods in combination with Factory methods and Row attributes. Verifying the correct routing to the Homecontroller might then look like this:

[TestFixture]

public class RouteFixture : RouteFixtureBase

{

    #region Fixture Setup/Teardown

 

    [FixtureSetUp]

    public void FixtureSetUp()

    {

        // Initialize the application's routing

        MvcApplication.RegisterRoutes(RouteTable.Routes);

    }

 

    #endregion // Fixture Setup/Teardown

 

    #region Route Factories

 

    public IEnumerable HomeControllerRoutes()

    {

        yield return CreateRouteActionPair<HomeController>("~/",           controller => controller.Index());

        yield return CreateRouteActionPair<HomeController>("~/Home",       controller => controller.Index());

        yield return CreateRouteActionPair<HomeController>("~/Home/Index", controller => controller.Index());

        yield return CreateRouteActionPair<HomeController>("~/Home/About", controller => controller.About());

    }

 

    public IEnumerable InvalidHomeControllerRoutes()

    {

        yield return CreateRouteActionPair<HomeController>("~/Home/NotExisting", controller => controller.Index());

        yield return CreateRouteActionPair<HomeController>("~/Index",            controller => controller.Index());

        yield return CreateRouteActionPair<HomeController>("~/default",          controller => controller.Index());

        yield return CreateRouteActionPair<HomeController>("~/About",            controller => controller.About());

    }

 

    #endregion // Route Factories

 

    #region Tests

 

    [Test, Factory("HomeControllerRoutes")]

    [Description("Verifies that the specified URLs map to the specified 'HomeController' actions.")]

    public void TestRouteMapsToHomeControllerAction(string url, Expression<Func<HomeController, ActionResult>> action)

    {

        AssertRouteMapsToControllerAction(url, action);

    }

 

    [Test, Factory("InvalidHomeControllerRoutes")]

    [Description("Verifies that the specified URLs do not map to the specified 'HomeController' actions.")]

    public void TestRouteDoesNotMapToHomeControllerAction(string url, Expression<Func<HomeController, ActionResult>> action)

    {

        AssertRouteDoesNotMapToControllerAction(url, action);

    }

 

    [Test, Description("Verifies that the specified URLs map to the 'HomeController' class.")]

    [Row("~/")]

    [Row("~/Home/NotExisting/13")]

    public void TestRouteMapsToHomeController(string url)

    {

        AssertRouteMapsToController<HomeController>(url);

    }

 

    [Test, Description("Verifies that the specified URLs does not map to the 'HomeController' class.")]

    [Row("~/blah")]

    [Row("~/34/not-existing")]

    public void TestRouteDoesNotMapToHomeController(string url)

    {

        AssertRouteDoesNotMapToController<HomeController>(url);

    }

 

    #endregion // Tests

 

} // class RouteFixture

This is testing the entire group of URLs that relate to the HomeController. But it's not only that this is easy to read and write/modify, these tests also give a much more expressive test report, from which one can immediately see, what exactly was tested. See here:

And it works also for arguments. The following test will fail, complaining about a wrong parameter value (I added an additional DataController to the project for demonstration purposes):

[Test]

[Description("Verifies that the URL maps to the 'DataController.Details' action with correct parameter value.")]

public void TestUrlMapsToDataControllerWithCorrectArguments()

{

    AssertRouteMapsToControllerAction<DataController>(

            "~/Data/Details/1",

            controller => controller.Details(2));

}

Limitations of this approach

I feel that I have to make some additional remarks about the limitations of the here outlined approach. From my own experience, I know that putting the routing mechanism under test can give a false sense of security: You know now how your application behaves in reaction to different URLs, huh? You don't! This test fixture is testing the application's RouteTable in isolation, so we know nothing about what happens later on. There may be redirects, renamings of controller actions, and the like. All this is beyond the scope of the above test fixture and can only be verified with some integration-style tests, because such behavior is the result of an ensemble of classes and/or configuration settings. Also, a test like this will pass:

[Test]

[Description("Verifies that the URL maps to the 'HomeController' class.")]

public void TestUrlMapsToHomeControllerRegardlessOfInvalidAction()

{

    AssertRouteMapsToController<HomeController>("~/Home/unknown");

}

Which is correct, if you think about it for a minute. It verifies that the RouteTable assumes an action method with name Unknown() to reside in the HomeController class. This is what we expect, the problem arises later, when  some code is actually trying to call Unknown()...

The sample solution

You can download the accompanying sample solution here (VS 2008). It contains the fully documented RouteFixtureBase class along with an exhaustive usage example.

 

kickit
shoutit
delicious facebook digg reddit linkedin stumbleupon technorati mrwong yahoo google-48x48 twitter email favorites
Posted on Monday, November 2, 2009 8:29 AM Unit Testing/TDD , ASP.NET (MVC) | Back to top


Comments on this post: Unit testing ASP.NET MVC routes

# re: Unit testing ASP.NET MVC routes
Requesting Gravatar...
Great Article ! Thanks
Left by Marc Chouteau on Nov 02, 2009 4:13 PM

# re: Unit testing ASP.NET MVC routes
Requesting Gravatar...
Thanks! This is great!

In the project I'm currently working on I'm testing the routing using Moq/MvcContric the way you describe it at the beginning of the article. It is hard to read and it doesn't buy much of the customer confidence. :(

I'm going to try your approach right away. It looks much cleaner and could improve the overall quality of the project.
Left by ms440 on Nov 04, 2009 6:01 PM

Your comment:
 (will show your gravatar)
 


Copyright © Thomas Weller | Powered by: GeeksWithBlogs.net | Join free