RouteEvaluator For Unit Testing Routes

asp.net, asp.net mvc 0 comments suggest edit

A while back I wrote a routing debugger which is useful for testing your routes and seeing which routes would match a given URL. Rob suggested we have something like this for unit tests, so I whipped something simple up.

This is a class that allows you to test multiple different URLs quickly. You simply create the RouteEvaluator giving it a collection of routes and then GetMatches which returns a List<RouteData> containing a RouteData instance for every route that matches, not just the first one.

Here’s a sample of usage.

[Test]
public void CanMatchUsingRouteEvaluator()
{
  var routes = new RouteCollection();
  GlobalApplication.RegisterRoutes(routes);

  var evaluator = new RouteEvaluator(routes);
  var matchingRouteData = evaluator.GetMatches("~/foo/bar");
  Assert.IsTrue(matchingRouteData.Count > 0);
  matchingRouteData = evaluator.GetMatches("~/foo/bar/baz/billy");
  Assert.AreEqual(0, matchingRouteData.Count);
}

And here’s the code. Note that my implementation relies on Moq, but you could easily implement it without using Moq if you wanted to.

public class RouteEvaluator
{
  RouteCollection routes;
    
  public RouteEvaluator(RouteCollection routes)
  {
    this.routes = routes;
  }

  public IList<RouteData> GetMatches(string virtualPath)
  {
    return GetMatches(virtualPath, "GET");
  }

  public IList<RouteData> GetMatches(string virtualPath, string httpMethod)
  {
    List<RouteData> matchingRouteData = new List<RouteData>();

    foreach (var route in this.routes)
    {
      var context = new Mock<HttpContextBase>();
      var request = new Mock<HttpRequestBase>();

      context.Expect(ctx => ctx.Request).Returns(request.Object);
      request.Expect(req => req.PathInfo).Returns(string.Empty);
      request.Expect(req => 
req.AppRelativeCurrentExecutionFilePath).Returns(virtualPath);
      if (!string.IsNullOrEmpty(httpMethod))
      {
        request.Expect(req => req.HttpMethod).Returns(httpMethod);
      }

      RouteData routeData = this.routes.GetRouteData(context.Object);
      if (routeData != null) {
        matchingRouteData.Add(routeData);
      }
    }
    return matchingRouteData;
  }
}

Let me know if this ends up being useful to you.

Technorati Tags: aspnetmvc,ASP.NET,routing

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

23 responses

  1. Avatar for Rob Conery
    Rob Conery May 5th, 2008

    OMFG I love you!

  2. Avatar for Vijay Santhanam
    Vijay Santhanam May 5th, 2008

    nice! you're a Moq convert aren't you? me too.

  3. Avatar for Simone Busoli
    Simone Busoli May 5th, 2008

    Very cool Phil, that's something I didn't think about, but looks very useful.

  4. Avatar for Pete
    Pete May 5th, 2008

    What the heck was that "GlobalApplication"?! Design-disaster and naming disaster.
    I've read somewhere "no global objects in OO". And the name GlobalApplication doesn't really describe anything. What is a NonGlobalApplication? Or LocalApplication.
    Crazy stuff. Otherwise it's all good.

  5. Avatar for Simone Busoli
    Simone Busoli May 5th, 2008

    Pete, it's just the name of the class behind Global.asax, nothing weird ;)

  6. Avatar for Troy DeMonbreun
    Troy DeMonbreun May 5th, 2008

    I'm presuming you guys wouldn't literally write tests that had a proliferation of URLs to verify?
    I.e., that the above unit test is just a simplified example and it would be better practice to write a much less brittle unit test, e.g.: reference an external file containing the URLs (and possibly other metadata) and write the test to iterate the file and output a global pass/fail (with maybe summary text of the URLs that failed)?

  7. Avatar for tgmdbm
    tgmdbm May 6th, 2008

    Love it. Nice one Phil.
    @Troy, yeah, I think using a csv file with url,routeName,failureMessage is a nice idea. Then you can write one test to rule them all. But this is really down to your own preference, and might not be applicable in certain projects.
    My only problem with testing urls is I don't really care what my urls look like, read: "I have no *good* reason to care". As long as when i call ActionLink passing in whatever values, it generates a url such that requesting that url will pass those values to my controller action. If that is true then I'm happy.
    So what I do is exactly that. I get the url from GetVirtualPath, and pass it back through GetRouteData and Assert that what i get out is what i put in. I don't test the url directly.

  8. Avatar for Steve
    Steve May 6th, 2008

    Has anyone managed to get the code to work correctly?
    Besides the fact that it doesn't compile (Calls to GetMatchingRoutes should be changed to GetMatches, or vice-versa), it always returns as many routes as I have registered in my RouteCollection, and they're always excatly the same routes! It looks like the foreach in GetMatches is redundant, but then I can't see how it can find multiple routes that match the same rule?
    Also, is there any way to find out whether the Controller returned for a route exists?

  9. Avatar for Haacked
    Haacked May 6th, 2008

    @Steve sorry, i had some typos in there. I fixed them up.

  10. Avatar for Guy
    Guy June 6th, 2008

    Thank You, every cool. The value added to an already great product just makes it that much more fantastic. I was affraid there would be no support for MVC, keep it up.
    A few points that need to be made clear for this to work. In the flurry of anxiety to get this to the web I think a "None Informed Use Case" was not done. I don't think a dry run of implmenting this was done. If one just takes what is give it does not work. No instructions on how to set it up. It was just assumed that we all know about [test] and that means NUnit (instead [TestMethod]) and we all know that GlobalApplication is the class name for Global.asx.cs which in some cases it is not. It may just be Global. Or as one persone points out using "Global" is bad and changed it to something completely different.
    Here are the preperation steps I think are missing.
    1. Make sure in the TestProject you make has a reference to your MVC Web Project (Company.Foo.WebApplication).
    2. Also make a reference to System.Web
    3. I could not use GlobalApplication.RegisterRoutes(routes); I had to do the following:
    Company.Foo.WebApplication.Global webApp = new Company.Foo.WebApplication.Global()
    webApp.RegisterRoutes(routes);

    Again Thank You.

  11. Avatar for Nick Berardi
    Nick Berardi July 8th, 2008

    Thanks for this.
    Just one bug I found.
    this.routes.GetRouteData(context.Object);
    should be
    route.GetRouteData(context.Object);

  12. Avatar for Matti Lehtinen
    Matti Lehtinen September 19th, 2008

    Thank you Nick about pointing out the bug. Helped me a lot.

  13. Avatar for Agus Suhanto
    Agus Suhanto September 27th, 2008

    Nick was right. Notice that in original code, route variable is not used within iteration block. And if we use this.routes instead of route local variable, the matching route count can be more than 1, but all are the same.

  14. Avatar for Agus Suhanto
    Agus Suhanto September 27th, 2008

    As addition, I think we can do more than just checking the count of matching route. If we want to be so precise to test which route is selected, the following code might be useful:
    var urlPattern = "{controller}/{action}";
    var matches = evaluator.GetMatches("~/commerce/test");
    Assert.IsTrue(matches.Count > 0);
    Assert.AreEqual(urlPattern, ((System.Web.Routing.Route)matches[0].Route).Url);

  15. Avatar for James World
    James World November 4th, 2008

    You can go even further than this and assert that particular parameters are correctly caught, eg to test the controller is foo in your example:

    Assert.AreEqual<string>(matchingRouteData[0].Values["controller"].ToString(), "foo");

  16. Avatar for Gopi
    Gopi February 18th, 2009

    Thanks a lot...

  17. Avatar for Andri
    Andri July 15th, 2010

    I still try to understand these whole article is all about, since I just learn it. Thanks for your great help. Andri

  18. Avatar for kenny
    kenny December 11th, 2010

    Hi, thanks for the article. I still try to understand the whole article is all about, coz still need to learn it. :D

  19. Avatar for ks
    ks January 23rd, 2011

    is it a major issue in rerouting. i still need time to understand it.

  20. Avatar for blogger
    blogger January 28th, 2011

    thats nice, keep posting useful tips friend.

  21. Avatar for susu kolostrum
    susu kolostrum April 21st, 2011

    My only problem with testing urls is I don't really care what my urls look like, read: "I have no *good* reason to care". As long as when i call ActionLink passing in whatever values, it generates a url such that requesting that url will pass those values to my controller action. If that is true then I'm happy.

  22. Avatar for jasa backlink
    jasa backlink April 21st, 2011

    I have registered in my RouteCollection, and they're always excatly the same routes! It looks like the foreach in GetMatches is redundant, but then I can't see how it can find multiple routes that match the same rule?

  23. Avatar for cheat lostsaga
    cheat lostsaga June 22nd, 2012

    Thanks a lot phil haack,..