ASP.NET MVC Tutorial: Testing Inbound Routes

in #utopian-io8 years ago (edited)

What Will I Learn?

In this tutorial, I am going to show you how we can use MvcContrib project’s route-testing API to make this testing easier.

Requirements

  • ASP.NET Core MVC
  • MvcContrib Test Helper

Difficulty

Intermediate

Tutorial Contents

  • Testing Inbound Routes
  • Testing routes the hard way
  • Installing the MvcContrib Test Helper via NuGet
  • Cleaner route testing with MvcContrib XE "MvcContrib" 's TestHelper project
  • Testing our example routes xe "Code Camp Server:testing routes"
  • The results of our route tests in the ReSharper XE "ReSharper" test runner

ASP.NET MVC Tutorial: Testing Inbound Routes

It's much easier to write automated tests for your site than it is to do manual exploratory testing for each release. In this tutorial, I am going to show you how we can use MvcContrib project’s route-testing API to make this testing easier.

Testing Inbound Routes

When compared with the rest of the ASP.NET MVC Framework, testing routes isn't easy or intuitive because of the number of abstract classes that need to be mocked. Doing this by hand requires a lot of setup code, as seen below.

Testing routes the hard way

using System.Web;
using System.Web.Routing;
using NUnit.Framework;
using Rhino.Mocks;
namespace RoutingSample.Tests
{
  [TestFixture]
  public class NotUsingTestHelper
  {
    [Test]
    public void root_matches_home_controller_index_action()
    {
        const string url = "~/";
        var request = MockRepository                               
            .GenerateStub<HttpRequestBase>();                      
        request.Stub(x => x.AppRelativeCurrentExecutionFilePath)   
           .Return(url).Repeat.Any();                              
        request.Stub(x => x.PathInfo)                              
            .Return(string.Empty).Repeat.Any();                    
        var context = MockRepository                               
            .GenerateStub<HttpContextBase>();                          
        context.Stub(x => x.Request)                                  
            .Return(request).Repeat.Any();                            
        RouteTable.Routes.Clear();                                    
        MvcApplication.RegisterRoutes(RouteTable.Routes);             
        var routeData = RouteTable.Routes.GetRouteData(context);       
        Assert.That(routeData.Values["controller"],                   
           Is.EqualTo("Home"));                                       
        Assert.That(routeData.Values["action"],                       
           Is.EqualTo("Index"));                                      
    }
  }
}

If all our route tests looked like listing 1, nobody would even bother testing routes. Those specific stubs on HttpContextBase and HttpRequestBase weren't lucky guesses either; it took a peek inside Red Gate's Reflector tool to find out what to mock. This isn't how a testable framework should behave!

Luckily, the MvcContrib project has a nice fluent route-testing API that we can use to make testing these routes easier. To begin, we'll need to ensure the MvcContrib.TestHelper assembly is installed by issuing the command Install-Package MvcContrib.Mvc3.TestHelper-ci in the NuGet Package Manager Console, as shown below.

Installing the MvcContrib Test Helper via NuGet

Cleaner route testing with MvcContrib XE "MvcContrib" 's TestHelper project

[TestFixtureSetUp]
public void FixtureSetup()
{
  RouteTable.Routes.Clear();
  MvcApplication.RegisterRoutes(RouteTable.Routes);                 
}
[Test]
public void root_maps_to_home_index()
{
   "~/".ShouldMapTo<HomeController>(x => x.Index());                
}

We begin by registering our application's routes in the test fixture's setup by using the static RegisterRoutes method from the Global.asax. The actual test itself is done with the magic and power of extension methods and lambda expressions. Inside MvcContrib's test helper there's an extension method on the string class that builds up a RouteData instance based on the parameters in the URL. The RouteData class has an extension method to assert that the route values match a controller and action.

You can see from listing 1 that the name of the controller is inferred from the generic type argument in the call to the ShouldMapTo<TController method. The action is then specified with a lambda expression. The expression is parsed to pull out the method call (the action) and any arguments passed to it. The arguments are matched with the route values. More information about these route testing extensions is available on the MvcContrib site at http://mvccontrib.org.

Now, it's time to apply this to our example store's routing rules and make sure that we've covered the desired cases.

Testing our example routes xe "Code Camp Server:testing routes"

using System.Web.Routing;
using MvcContrib.TestHelper;
using NUnit.Framework;
using RoutingSample.Controllers;
namespace RoutingSample.Tests
{
    [TestFixture]
    public class UsingTestHelper
    {
        [TestFixtureSetUp]
        public void FixtureSetup()
        {
            RouteTable.Routes.Clear();
            MvcApplication.RegisterRoutes(RouteTable.Routes);
        }
        [Test]
        public void root_maps_to_home_index()
        {
            "~/".ShouldMapTo<HomeController>(x => x.Index());
        }
        [Test]
        public void privacy_should_map_to_home_privacy()
        {
            "~/privacy".ShouldMapTo<HomeController>(x => x.Privacy());
        }
        [Test]
        public void products_should_map_to_catalog_index()
        {
            "~/products".ShouldMapTo<CatalogController>(x => x.Index());
        }
        [Test]
        public void product_code_url()
        {
            "~/products/product-1".ShouldMapTo<CatalogController>(
               x => x.Show("product-1"));
        }
        [Test]
        public void product_buy_url()
        {
            "~/products/product-1/buy".ShouldMapTo<CatalogController>(
               x => x.Buy("product-1"));
        }
        [Test]
        public void basket_should_map_to_catalog_basket()
        {
            "~/basket".ShouldMapTo<CatalogController>(
               x => x.Basket());
        }
        [Test]
        public void checkout_should_map_to_catalog_checkout()
        {
            "~/checkout".ShouldMapTo<CatalogController>(
               x => x.CheckOut());
        }
        [Test]
        public void _404_should_map_to_error_notfound()
        {
            "~/404".ShouldMapTo<ErrorController>(
                x => x.NotFound());
        }
        [Test]
        public void ProductsByCategory_MapsToWebFormPage()
        {
            "~/Products/ByCategory"
               .ShouldMapToPage("~/ProductsByCategory.aspx");
        }
    }
}

Each of these simple test cases uses the NUnit testing framework. They also use the ShouldMapTo extension method found in MvcContrib.TestHelper.

Note

The final test uses a method that's different from the MvcContrib TestHelper. The ShouldMapToPage method ensures that a URL maps to a particular Web Forms page. This would also catch routing errors. If you have unit tests for your routes, then you'll probably spend less time needing to debug them.

After running this example, we can see that all our routes are working properly. Figure 2 shows the ReSharper test runner results. (The output may look slightly different depending on your testing framework and runner.)

The results of our route tests in the ReSharper XE "ReSharper" test runner

Note

We separated each rule into its own test. It might be tempting to keep all of these one-liners in a single test, but don't forget the value of understanding why a test is failing. If you make a mistake, only distinct tests will break, giving you much more information than a single broken test_all_routes() test.

Summary

Armed with these tests, we're free to modify our route rules, confident that we aren't breaking existing URLs on our site. Imagine if product links on Amazon.com were suddenly broken due to a typo in some route rule... Don't let that happen to you. It's much easier to write automated tests for your site than it is to do manual exploratory testing for each release.



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @alv I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Coin Marketplace

STEEM 0.04
TRX 0.32
JST 0.090
BTC 62154.06
ETH 1736.43
USDT 1.00
SBD 0.38