尽可能摆脱对HttpContext的依赖
在ASP.NET中进行单元测试的天敌便是HttpContext,它是ASP.NET的核心,极端复杂,却无法进行Mock1——可见微软能够写出那么庞大的ASP.NET框架真不那么容易。现在这个状况改善了不少,因此大家已经可以使用System.Web.Abstractions.dll了,这个程序集中提供了对于HttpContext的抽象,也就是HttpContextBase抽象类。因此在ASP.NET MVC中,各种组件均依赖于HttpContextBase而不是HttpContext。这是一个优秀的做法,大家以后可以尽可能地摆脱HttpContext了。
不过这似乎又是一个悖论。虽然已经可以对HttpContext进行Mock(这点增强了可测试性),但是过度依赖HttpContext对于单元测试来说也是一个伤害。这是HttpContext对象的天性所致:它实在太复杂了。您应该已经察觉到,这是个集万千宠爱于一身的对象,从请求,回复,应用程序,缓存……几乎包含了Web应用程序需要的所有信息。如果要测试一个依赖于HttpContext的方法,您势必要为HttpContext的Mock对象填充各种信息——其复杂程度视业务而定。而且,Mock关注的是“行为”,也就是说它关注的是做一件事情所使用“路径”。那么如果做一件事情可以采用多个路径又会怎样是否需要在测试之前准备好所有的路径,并且验证被测试的代码“采用了,并仅仅采用了其中一条路径”因此,Stub慢慢进入人们的视线。Stub关注的是“状态”……这就是另一个话题了,还会涉及到采用Record & Replay还是Arrange-Act-Assert方式来进行单元测试,暂且不提。
之前谈到对视图进行单元测试时,老赵曾经谈起在视图中应该只使用ViewData中的数据。这不是第一次说起要放弃HttpContext了,自从有了“抽象”这一有利武器后,一切“不和谐”因素都能够被分离。试想在MVP模式中,View和Presenter都使用各自的抽象进行交互,一切Web控件,HttpContext等对象都不复存在了,大家眼中只有“数据”和“模型”。同样,在ASP.NET MVC的Action方法中,也不应该使用HttpContext,这是基于良好的“可测试性”而考虑的。您可能会想,现在的HttpContextBase对象已经可以Mock了啊。没错,它的确“可以”,但是这样做会引起单元测试代码的膨胀,因为测试代码中的相当部分必须关注在测试数据的准备,而不是被测试的功能上。对于一个Action方法来说,它关注的应该是用户与业务逻辑的交互,而不是“如何把HTTP请求转化为可用的数据”。其实说到底,还是要“分离关注点”。
在ASP.NET MVC中负责“转化数据”的层次为Model Binder。关于这一点,现有的“示例”大都关注把Form或QueryString中的数据转化为Action参数上,不过Model Binder可用的地方其实更多。例如在《最佳实践》的代码中,原本AccountController的Delete方法实现如下:
public ActionResult Delete(string userName)
{
this.MiddleTier.UserManager.Delete(userName);Uri urlReferrer = this.Request.UrlReferrer;
return this.Redirect(urlReferrer.ToString());
}
在删除了指定对象之后,页面将跳转到Url Referrer地址中。在上面的代码中,这个值将通过访问Request.UrlReferer来获得。这就使您的Action方法与HttpContext产生了依赖,因此它的单元测试代码就需要这样编写:
[TestMethod]
public void DeleteTest()
{
string userName = "jeffz";
Uri urlReferrer = new Uri("http://www.microsoft.com");var mockHttpContext = new Mock<HttpContextBase>();
mockHttpContext.Setup(c => c.Request.UrlReferrer).Returns(urlReferrer);var mockController = this.GetMockController();
mockController.Setup(c => c.MiddleTier.UserManager.Delete(userName)).Verifiable();
mockController.Object.ControllerContext = new ControllerContext(
mockHttpContext.Object, new RouteData(), mockController.Object);mockController.Object.Delete(userName)...
}
在单元测试代码中,我们Mock了一个HttpContextBase对象,让它的Request.UrlReferrer属性返回我们准备好的对象,再构造一个新的ControllerContext并交给Controller。而如果我们的UrlReferrer能够作为Delete方法的参数,那么单元测试代码就会一下子简单很多:
[TestMethod()]
public void DeleteTest()
{
string userName = "jeffz";
Uri urlReferrer = new Uri("http://www.microsoft.com");var mockController = this.GetMockController();
mockController.Setup(c => c.MiddleTier.UserManager.Delete(userName)).Verifiable();mockController.Object.Delete(userName, urlReferrer)...
}
有些朋友可能会问,不就是从Request的UrlReferrer属性中取值吗我们为什么要构造一个ControllerContext,不能直接设置Controller对象吗例如这样就简单多了:
mockController.Setup(c => c.Request.UrlReferrer).Returns(urlReferrer);
似乎可行,不过您运行的时候就会发现,框架会抛出异常,说只有接口的成员,或可以override的成员才能够被Mock。没错,Controller的Request属性不是virtual的,无法override。Controller类如此设计是故意的,目的就是限制了可用的路径。试想,如果您Mock了Controller.Request属性,但是程序代码通过Controller.HttpContext.Request进行访问又怎么办呢类似的做法还有对方法重载的设计。
一般来说,都会把其中几个方法委托给其中唯一的方法,而只有那个方法是可以被override的。这样在编写测试时,我们仅有的Mock入口便确定了,避免了测试代码过度了解方法实现的问题。
回到正题。如果要让Delete方法接urlReferrer受参数,那么我们就要编写Model Binder相关的组件:
public class UrlReferrerModelBinder : IModelBinder
{
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
return controllerContext.HttpContext.Request.UrlReferrer;
}}
并使其可以直接运用到Action的参数上:
public class UrlReferrerAttribute : CustomModelBinderAttribute
{
private static UrlReferrerModelBinder s_modelBinder =
new UrlReferrerModelBinder();public override IModelBinder GetBinder()
{
return s_modelBinder;
}
}
于是乎,我们的Delete方法便可写为:
public ActionResult Delete(string userName, Uri urlReferrer)
{
this.MiddleTier.UserManager.Delete(userName);
return this.Redirect(urlReferrer.ToString());
}
如今的代码,无论是应用程序还是框架类库,都必须考虑“可测试性”
补充:Web开发 , ASP.Net ,