当前位置:首页 > [翻译] ASP.NET MVC Tip #13 – 对自定义路由进行单元测试

[翻译] ASP.NET MVC Tip #13 – 对自定义路由进行单元测试

点击次数:1123  更新日期:2010-12-31
\n

原文地址:http://weblogs.asp.net/stephenwalther/archive/2008/07/02/asp-net-mvc-tip-13-unit-test-your-custom-routes.aspx


\n

翻译:Anders Liu


\n

摘要:在这个Tip中,Stephen Walther演示了如何为你的ASP.NET MVC应用程序中的自定义路由创建单元测试。Stephen Walther介绍了如何测试一个URL是否被映射到正确的控制器、控制器操作和操作参数上。


\n

在创建ASP.NET MVC应用程序时,如果你是忠于测试驱动开发的,你应该对所有东西进行单元测试。先编写单元测试,再编写代码来满足测试。重复、重复、重复到吐。


\n

路由是MVC应用程序中的重要部分。路由决定了一个URL如何映射到特定的控制器和控制器操作。由于路由在MVC应用程序中如此重要,所以你需要为路由编写单元测试。在这个Tip中,我将向你展示如何通过仿制HTTP Context来为路由编写单元测试。


\n

创建路由表
你可以在Global.asax文件中为MVC应用程序创建路由。换句话说,它们是定义在GlobalApplication类中的。清单1包含了默认的Global.asax文件。


\n

清单1 – Global.asax


\n

view plaincopy to clipboardprint?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace DefaultOne
{
public class GlobalApplication : 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
);

}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace DefaultOne
{
public class GlobalApplication : 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
);

}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}

默认情况下,一个路由包括名字、路径和默认路由。路由得到一个URL后会将其中的不同片段映射到特定的控制器、控制器操作和传递给操作的参数上。如:


\n

/Customer/Details/23
Controller = Customer
Action = Details
Id = 23
URL中的第一个片段被映射到控制器名字,第二部分被映射到控制器操作,而最后一部分被映射到名字为Id的参数。


\n

Default路由包含了默认值。如果没有指定控制器,则使用Home控制器。如果没有指定操作,则调用Index操作。如果没有指定Id,则传第一个空字符串。因此,下面的URL将被这样解释:


\n

/
Controller = Home
Action = Index
Id = “”
对很多MVC应用程序来说,默认路由是你经常要用到的一个。然而,你还可以选择创建自定义路由。例如,清单2中的Global.asax文件包含了两个自定义路由。


\n

清单2 – Global.asax


\n

view plaincopy to clipboardprint?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace Tip13
{
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute(“{resource}.axd/{*pathInfo}”);

// Route for blog archive
routes.MapRoute(
“Archive”, // Name
“Archive/{entryDate}”, // URL
new { controller = “Blog”, action = “Details” }, // Defaults
new { entryDate = @”\\d{2}-\\d{2}-\\d{4}” } // Constraints
);

// Default route
routes.MapRoute(
“Default”, // Name
“{controller}/{action}/{id}”, // URL
new { controller = “Home”, action = “Index”, id = “” } // Defaults
);

// Catch all route
routes.MapRoute(
“CatchIt”, // Name
“Product/{*values}”, // URL
new { controller = “Product”, action = “Index” } // Defaults
);


}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace Tip13
{
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute(“{resource}.axd/{*pathInfo}”);

// Route for blog archive
routes.MapRoute(
“Archive”, // Name
“Archive/{entryDate}”, // URL
new { controller = “Blog”, action = “Details” }, // Defaults
new { entryDate = @”\\d{2}-\\d{2}-\\d{4}” } // Constraints
);

// Default route
routes.MapRoute(
“Default”, // Name
“{controller}/{action}/{id}”, // URL
new { controller = “Home”, action = “Index”, id = “” } // Defaults
);

// Catch all route
routes.MapRoute(
“CatchIt”, // Name
“Product/{*values}”, // URL
new { controller = “Product”, action = “Index” } // Defaults
);


}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}

清单2中修改过的Global.asax包含了一个新的、名为Archive的路由,用于处理类似下面这样的对blog文章的请求:


\n

/archive/12-25-1966
该自定义路由将这个URL映射到名为Blog的控制器并调用Details()操作。日期将作为名为entryDate的参数传递到Details()操作。


\n

这个Global.asax文件还定义了一个catchall路由。catchall路由包含任意数量的片段。例如,catchall路由将会匹配:


\n

/Product/a
/Product/a/b
/Product/a/b/c
以此类推。


\n

对自定义路由进行单元测试
那么如何测试自定义路由呢?在我从xUnit(http://www.codeplex.com/xUnit)中看到一个MVC单元测试示例之前我无法指出如何做这样的单元测试。为了测试自定义路由,你需要仿制HTTP Context。


\n

在上一篇Tip中,我介绍了在对ASP.NET内部对象如会话状态、表单参数和用户实体/角色进行单元测试时,如何仿制上下文对象。如果你还没有读过这篇Blog,请访问下面的页面:


\n

http://www.cnblogs.com/AndersLiu/archive/2008/07/26/asp-net-mvc-tip-12-faking-the-controller-context.html
在看过了xUnit的示例之后,我修改了仿制的上下文对象,使其能够用于对路由进行单元测试。清单3中的单元测试演示了如何对清单2中的Global.asax中包含的自定义路由进行测试。


\n

清单3 – RouteTest.cs


\n

view plaincopy to clipboardprint?
using System;
using System.Web.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcFakes;
using Tip13;

namespace Tip13Tests.Routes
{
[TestClass]
public class RoutesTest
{
[TestMethod]
public void TestDefaultRoute()
{
// Arrange
var routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);

// Act
var context = new FakeHttpContext(“~/”);
var routeData = routes.GetRouteData(context);

// Assert
Assert.AreEqual(“Home”, routeData.Values["controller"], “Default controller is HomeController”);
Assert.AreEqual(“Index”, routeData.Values["action"], “Default action is Index”);
Assert.AreEqual(String.Empty, routeData.Values["id"], “Default Id is empty string”);
}

[TestMethod]
public void TestGoodArchiveRoute()
{
// Arrange
var routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);

// Act
var context = new FakeHttpContext(“~/Archive/12-25-1966″);
var routeData = routes.GetRouteData(context);

// Assert
Assert.AreEqual(“Blog”, routeData.Values["controller"], “Controller is Blog”);
Assert.AreEqual(“Details”, routeData.Values["action"], “Action is Details”);
Assert.AreEqual(“12-25-1966″, routeData.Values["entryDate"], “EntryDate is date passed”);

}

[TestMethod]
public void TestBadArchiveRoute()
{
// Arrange
var routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);

// Act
var context = new FakeHttpContext(“~/Archive/something”);
var routeData = routes.GetRouteData(context);

// Assert
Assert.AreNotEqual(“Blog”, routeData.Values["controller"], “Controller is not Blog”);
}

[TestMethod]
public void TestCatchAllRoute()
{
// Arrange
var routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);

// Act
var context = new FakeHttpContext(“~/Product/a/b/c/d”);
var routeData = routes.GetRouteData(context);

// Assert
Assert.AreEqual(“a/b/c/d”, routeData.Values["values"], “Got catchall values”);
}
}
}
using System;
using System.Web.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcFakes;
using Tip13;

namespace Tip13Tests.Routes
{
[TestClass]
public class RoutesTest
{
[TestMethod]
public void TestDefaultRoute()
{
// Arrange
var routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);

// Act
var context = new FakeHttpContext(“~/”);
var routeData = routes.GetRouteData(context);

// Assert
Assert.AreEqual(“Home”, routeData.Values["controller"], “Default controller is HomeController”);
Assert.AreEqual(“Index”, routeData.Values["action"], “Default action is Index”);
Assert.AreEqual(String.Empty, routeData.Values["id"], “Default Id is empty string”);
}

[TestMethod]
public void TestGoodArchiveRoute()
{
// Arrange
var routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);

// Act
var context = new FakeHttpContext(“~/Archive/12-25-1966″);
var routeData = routes.GetRouteData(context);

// Assert
Assert.AreEqual(“Blog”, routeData.Values["controller"], “Controller is Blog”);
Assert.AreEqual(“Details”, routeData.Values["action"], “Action is Details”);
Assert.AreEqual(“12-25-1966″, routeData.Values["entryDate"], “EntryDate is date passed”);

}

[TestMethod]
public void TestBadArchiveRoute()
{
// Arrange
var routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);

// Act
var context = new FakeHttpContext(“~/Archive/something”);
var routeData = routes.GetRouteData(context);

// Assert
Assert.AreNotEqual(“Blog”, routeData.Values["controller"], “Controller is not Blog”);
}

[TestMethod]
public void TestCatchAllRoute()
{
// Arrange
var routes = new RouteCollection();
GlobalApplication.RegisterRoutes(routes);

// Act
var context = new FakeHttpContext(“~/Product/a/b/c/d”);
var routeData = routes.GetRouteData(context);

// Assert
Assert.AreEqual(“a/b/c/d”, routeData.Values["values"], “Got catchall values”);
}
}
}

这是Visual Studio Test(MS Test)单元测试。当然你也可以使用不同的测试框架,如NUnit或xUnit。下面是这个单元测试工作的方式。


\n

首先,新建了一个路由集合,并将其传递给Global.asax文件中定义的RegisterRoutes()方法。Global.asax文件对应着一个名为GlobalApplication的类。


\n

接下来,创建了一个仿制的HTTP Context,其中包含了待测试的URL。例如,在等一个测试中,URL ~/Archive/12-25-1966被传递到仿制的HTTP Context对象的构造器中。仿制的HTTP Context对象是我在Tip #12中创建的仿制MVC对象的修改版。本文后面可以下载到源代码,其中的MvcFakes项目中包含了这些仿制对象。


\n

接下来,在仿制的上下文上到用了GetRouteData()方法,并返回了路由数据。路由数据表示将URL传递给应用程序路由表,经过解释后得到的结果。换句话说,路由数据是将URL与路由表中的路由进行对比后得到的结果。


\n

最后,该测试判断路由数据中是否包含需要的值。在第一个测试里,验证了控制器名字、控制器操作和Id的值。依照该测试,空的URL ~/应该映射到Home控制器、Index操作,并且Id的值是String.Empty。


\n

第二个测试检查了类似~/Archive/12-25-1966这样的请求是否映射到Blog控制器、Details操作,并创建了名为entryDate的操作。


\n

第三个测试检查了类似~/Archive/something这样的请求不能映射到Blog控制器。因为该URL不包含恰当的entryDate,所以不能被Blog控制器处理。


\n

最后一个测试验证了catchall路由能够正确工作。该测试检查了~/Product/a/b/c/d得到了解析,使得values参数等于a/b/c/d。换句话说,它检查了catchall控制器的catch-all部分。


\n

小结
在这个Tip中,我向你展示了一种简单的测试自定义ASP.NET MVC路由的方法。我建议任何时候只要你修改了Global.asax文件中的默认路由,都应该对其进行单元测试。


\n

此处下载源代码:http://weblogs.asp.net/blogs/stephenwalther/Downloads/Tip13/Tip13.zip。

来源: http://www.cnblogs.com/AndersLiu/

\n