About this Project
Have you ever gotten your MVC application's routes working exactly the way you want -- only to add a new pattern in your
Global.asax that screws everything up? This used to happen to me all the time; often without realizing it until well after the fact. Out of that frustration,
MVC Route/URL Generation Unit Tester was born.
MVC Route/URL Generation Unit Tester provides convenient, easy to use methods that let you unit test the route table in your ASP.NET MVC application. Unlike many libraries, this lets you test routes
both ways -- both incoming and outgoing. You can specify an incoming request and make one of several assertions about the outcome; such that the request matches a given route, matches no routes, or that it is ignored by the routing system. You can also
specify route data and assert that a given URL will be generated by your application.
MVC Route/URL Generation Unit Tester works with any unit testing library you choose, such as MSTest, NUnit, xUnit, MbUnit, and others.
Important Changes
Be sure to check frequently for new feature updates while the library remains in beta. Any significant updates will be noted on the
Change Log page. You can also follow me
@leedumond on Twitter for any important change notifications.
Step 1: Install the Library via
Nuget
From within your unit test project,
PM> Install-Package MvcRouteUnitTester or via the Nuget management tools in Visual Studio.
Step 2: Test Your Incoming Routes
Here, you will set up a test for your incoming routes.
using MvcRouteUnitTester;
[TestClass]
public class RouteTests
{
[TestMethod]
public void TestIncomingRoutes()
{
// Arrange
var tester = new RouteTester<MvcApplication>();
// Assert
tester.WithIncomingRequest("/").ShouldMatchRoute("Home", "Index");
tester.WithIncomingRequest("/Foo").ShouldMatchRoute("Foo", "Index");
tester.WithIncomingRequest("/Foo/Index").ShouldMatchRoute("Foo", "Index");
tester.WithIncomingRequest("/Foo/Bar").ShouldMatchRoute("Foo", "Bar");
tester.WithIncomingRequest("/Foo/Bar/5").ShouldMatchRoute("Foo", "Bar", new { id = 5 });
tester.WithIncomingRequest("/Foo/Bar/5/Baz").ShouldMatchNoRoute();
tester.WithIncomingRequest("/handler.axd/pathInfo").ShouldBeIgnored();
}
}
First, you arrange the test by instantiating a new
RouteTester that you can use to test your routes. You do that by invoking the generic constructor and passing in the name of your
HttpApplication class in
Global.asax (when you create a new application, this class is named
MvcApplication by default).
After you've done that, you can specify the incoming request you want to test by passing the request URL to the
WithIncomingRequest method. You then assert using the
ShouldMatchRoute method, passing in the names of the action and controller you expect the given request to match. You will also pass in the URL parameter values from the request, as well as the default values from the route
you expect to match, using an anonymous object. If the assertion fails, the
ShouldMatchRoute method throws an
AssertionException, whose message will reveal the reason the failure occurred.
If you want to confirm that a given request will NOT be matched by any route, you can call the
ShouldMatchNoRoute method (this is handy when testing constrained routes). If you want to confirm that a request is ignored by the routing system, you can call the
ShouldBeIgnored method. (Note that these are two
different conditions, as an ignored route still counts as a match).
NOTE: The
WithIncomingRequest method also accepts an optional string parameter that allows you to specify the HTTP method of the request (with a default of
"GET".) This is handy for testing routes constrained using an
HttpMethodConstraint. Note that this is
not the same as restricting action methods using
HttpGetAttribute or
HttpPostAttribute. Remember: this library tests your
routes, not your action methods. :-)
Step 3: Test Your Outgoing Routes (URL Generation)
Then, you can set up a test for your outgoing routes.
using MvcRouteUnitTester;
[TestClass]
public class RouteTests
{
[TestMethod]
public void TestIncomingRoutes()
{
// see Step 2 above...
}
[TestMethod]
public void TestOutgoingRoutes()
{
// Arrange
var tester = new RouteTester<MvcApplication>();
// Assert
tester.WithRouteInfo("Home", "Index").ShouldGenerateUrl("/");
tester.WithRouteInfo("Home", "About").ShouldGenerateUrl("/Home/About");
tester.WithRouteInfo("Home", "About", new { id = 5 }).ShouldGenerateUrl("/Home/About/5");
tester.WithRouteInfo("Home", "About", new { id = 5, someKey = "someValue" }).ShouldGenerateUrl("/Home/About/5?someKey=someValue");
}
}
As before, you arrange the test by instantiating a
RouteTester to work with.
You specify the routing data you want to test against by passing the name of the action, the controller, and any additional route values to the
WithRouteInfo method. You then assert using the
ShouldGenerateUrl method, passing in the URL you expect your application to generate with the given information. If the assertion fails, the
ShouldGenerateUrl method throws an
AssertionException that reveals the reason the failure occurred.
IMPORTANT: The
ShouldGenerateUrl method is
case-sensitive. Make sure the string you pass to the
ShouldGenerateUrl method is cased as the application provides, otherwise the test will fail. For example, if you're using this in conjunction with my
LowercaseRoutesMVC utility, you'd want to pass in lower case URLs, like
.ShouldGenerateUrl("/home/about").
Step 4: Testing Area Routes
You may have noticed that the
ShouldMatchRoute and
WithRouteInfo methods have overloads that let you supply an area argument as a string. Those are the methods to use when you want to test your Area routes.
For the purposes of this discussion, we will assume you have added an area called "Admin" to your application. When you did that, a folder named
Areas was added to your application, and inside that folder another folder named
Admin. Inside that
Admin folder, there is a class called
AdminAreaRegistration in which you would define the routes for your Admin area.
Here is what the route tests for your Admin area might look like:
using MvcRouteUnitTester;
[TestClass]
public class AdminAreaRouteTests
{
[TestMethod]
public void TestIncomingRoutes()
{
// Arrange
var tester = new RouteTester<AdminAreaRegistration>();
// Assert
tester.WithIncomingRequest("/Admin/Foo").ShouldMatchRoute("Admin", "Foo", "Index");
tester.WithIncomingRequest("/Admin/Foo/Index").ShouldMatchRoute("Admin", "Foo", "Index");
tester.WithIncomingRequest("/Admin/Foo/Bar").ShouldMatchRoute("Admin", "Foo", "Bar");
tester.WithIncomingRequest("/Admin/Foo/Bar/5").ShouldMatchRoute("Admin", "Foo", "Bar", new { id = 5 });
tester.WithIncomingRequest("/Admin").ShouldMatchNoRoute();
tester.WithIncomingRequest("/Admin/Foo/Bar/5/Baz").ShouldMatchNoRoute();
}
[TestMethod]
public void TestOutgoingRoutes()
{
// Arrange
var tester = new RouteTester<AdminAreaRegistration>();
// Assert
tester.WithRouteInfo("Admin", "Foo", "Bar").ShouldGenerateUrl("/Admin/Foo/Bar");
tester.WithRouteInfo("Admin", "Foo", "Bar", new { id = 5 }).ShouldGenerateUrl("/Admin/Foo/Bar/5");
}
}
This time, you instantiate the
RouteTester by passing in the class that contains your Area route definitions. The testing procedure is pretty much the same -- except of course, you will be using the overloads of
ShouldMatchRoute and
WithRouteInfo that take the area string argument.
Customizing the HTTP context used for tests
The
WithRouteInfo and
WithRequestInfo methods use an HTTP context which is mocked up internally for the purposes of performing the test. For the vast majority of cases, this is something you don't need to be concerned about. However, there may be some specialized cases
where you need to alter or customize the context (For an example, see
Additional setup in TestUtility.GetHttpContext(..)).
For these special use cases, the
RouteInfo and
RequestInfo classes expose an
HttpContext property which allows you to obtain the underlying context. From there, you can extend the context as you desire, or even obtain the underlying mock of it (using
Mock.Get()) to create additional setups.
Questions? Issues? Suggestions?
Feel free to post them under the
Discussions tab above.