zoukankan      html  css  js  c++  java
  • mvc:尽可能摆脱对HttpContext的依赖

      本文出自:http://www.cnblogs.com/JeffreyZhao/archive/2009/03/09/no-dependency-to-httpcontext.html

      我们继续《ASP.NET MVC单元测试最佳实践》,今天主要谈论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());}

      如今的代码,无论是应用程序还是框架类库,都必须考虑“可测试性”这个要求。例如.NET 3.0的WF,由于其可测试性不佳一直为人所诟病。现在我们在编写程序时,要时刻询问自己:“这么做方便测试吗?”考虑到这个问题,可能您就会放心地做出某些抉择了2

    注1:其实还是可以Mock的。例如Typemock使用Profiler的方式进行直接注入,可以Mock任何成员。不过,如果Moq等框架无法满足您的需要,一般便是您的设计有些问题了。

    注2:例如,究竟让Action方法返回ActionResult,还是返回void,并直接通过Response输出呢?

  • 相关阅读:
    获取Oracle、SqlServer、Access中表名、字段和主键(转)
    Oracle事务控制总结
    Oracle数据类型
    Oracle中的数据字典技术及常用数据字典总结
    asp.net中的<%%>形式的详细用法总结
    一道sql面试题的解答
    求ax(2)+bx+c=0的解
    求发票的下一个号码
    软件设计师2008年12月下午试题4(C语言 动态规划)
    软件设计师1991下午试题1(流程图解析)
  • 原文地址:https://www.cnblogs.com/dudu837/p/1888035.html
Copyright © 2011-2022 走看看