Sunday, November 2, 2014

TDD with Routes and RouteConfig in MVC 5

When doing TDD on an MVC app it's important to not neglect the code that's contained in the RouteConfig class. This is especially true if complex URLs are used to enhance the user experience with using and sharing URLs. Because this took me a little while to figure out, and because the process changed with MVC 5 (confusing google searches on the subject), here are some example tests to use in your application.

These examples are using Asp.Net MVC 5, NUnit, and Moq, but can be tailored to whatever testing and mocking frameworks you use.  

For the following tests this is the RouteConfig.RegisterRoutes method used:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
        name: "Account",
        url: "Account/",
        defaults: new { controller = "Account", action = "Index" }
        );

    routes.MapRoute(
        name: "AccountWithAccountNo", 
        url: "Account/{accountNo}",
        defaults: new {controller="Account", action="Detail" }
        );
    routes.MapRoute(
        name: "AccountToUser",
        url: "Account/{accountNo}/User",
        defaults: new {controller = "User", action="Index"}
        );            
    routes.MapRoute(
        name: "AccountToUserWithGuid",
        url: "Account/{accountNo}/User/{userGuid}",
        defaults: new { controller = "User", action = "Edit" }
        );
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
} 

The trick that really makes this work is the AppRelativeCurrentExecutionFilePath property on the  HttpRequest object.  This property normally returns the path of the request  in a relative way (i.e. "~/Account").  This is helpful since it means you don't need to completely flesh out the HttpContext and Request objects in order to make your tests work, and it allows you to write the tests quickly.

To do a basic test for a URL path it's necessary to first Mock the HttpContextBase and then Mock up the Request object so that it returns the Path that is going to be tested. The Assert statements perform the tests to insure that the correct controller and action are called and that the correct parameters are included. In this first example, no extra parameters should be included.

[Test()]
public void RouteConfig_Account_Index_Test()
{
    //Arrange        
    //Create the mocked HttpContextBase
    var httpContextMock = new Mock();
    //Mock the return for the Request's URL.  The string value is the Relative Path
    //of the URL that would have been used.
    httpContextMock.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath)
                   .Returns("~/Account");            
    var routes = new RouteCollection();
    RouteConfig.RegisterRoutes(routes);           

    //Act
    RouteData routeData = routes.GetRouteData(httpContextMock.Object); 

    //Assert
    Assert.IsNotNull(routeData);
    //Tests to make sure the right controller will be called
    Assert.AreEqual(routeData.Values["controller"], "Account");
    //Tests to make sure the right action will be called
    Assert.AreEqual(routeData.Values["action"], "Index");
    //Tests to make sure that there is no id value given to the action used
    Assert.IsNull(routeData.Values["id"]);  
}

​In this next example, a more complex url is used and the test makes sure that the correct controller and action are called, and that both parameters are successfully included. 

[Test()]
public void RouteConfig_User_WithAccountNoAndUserGuid_Test()
{ 

    //Arrange        
    var httpContextMock = new Mock();
    //In this test a more complex url is used which includes the accountNo as well as the user
    //guid.  The Asserts below test to make sure that the right controller and action are called as well
    //as that the values are passed in correctly.  
    httpContextMock.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath)
                   .Returns("~/Account/54321/User/788CB225-AD01-4D33-B724-5AC7220F5864");      

    var routes = new RouteCollection();
    RouteConfig.RegisterRoutes(routes); 

    //Act
    RouteData routeData = routes.GetRouteData(httpContextMock.Object); 

    //Assert
    Assert.IsNotNull(routeData);
    Assert.AreEqual(routeData.Values["controller"], "User");
    Assert.AreEqual(routeData.Values["action"], "Edit");
    Assert.IsNotNull(routeData.Values["accountNo"]);
    Assert.AreEqual(routeData.Values["accountNo"], "54321");
    Assert.IsNotNull(routeData.Values["userGuid"]);
    Assert.AreEqual(routeData.Values["userGuid"], "788CB225-AD01-4D33-B724-5AC7220F5864");
}


When using TDD on an MVC application, it's important to not neglect testing the RouteConfig object. These examples should help you do that.  

No comments:

Post a Comment