我们继续《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输出呢?

艾伟:尽可能摆脱对HttpContext的依赖相关推荐

  1. 华米已成全球第五大智能手表企业,可望进一步摆脱对小米的依赖

    据市调机构counterpoint公布的2019年一季度的智能手表市场的数据显示,小米生态链企业华米的自主品牌AMAZFIT智能手表位居第五名,显示出它推出的智能手表已取得一定的成功,这有助于它实现摆 ...

  2. 苹果研发microLED暂难摆脱对三星的依赖

    一面是苹果正在自研microLED,一面是苹果今年将从三星采购更多的OLED面板,这说明了它虽然积极介入面板技术研发但是短期内它还难以摆脱对三星面板的依赖. 苹果积极介入面板行业 去年苹果推出的iPh ...

  3. 首次摆脱对梯度的依赖,CMU等开源Score-CAM:基于置信分数的视觉可解释性

    本文介绍一篇被CVPRW2020接受的论文,主要关于一种基于置信分数的视觉可解释性方法.本文的亮点在于:在CAM系列方法的基础上,首次提出了一种新的gradient-free的权重表达方式. 本文首发 ...

  4. 依赖倒置原则_C#教您一步步摆脱面向过程:依赖倒置

    前面有几篇文章,已经教了一些方式或者方法,帮助您摆脱娘胎自带的面向过程编程,从而转为面向对象. 本文讲一下面向对象依赖倒置,使用具体的例子来解释可能好些,网上千篇一律的大多数文字描述,各位估计也不一定 ...

  5. 苹果摆脱对中国制造的依赖?iPhone14的拆解结果显示恰恰相反,更离不开中国制造了...

    由于苹果在行业内所具有的强大影响力,因此iPhone14上市以来的热度一直高居不下,其中一个焦点是苹果与中国制造的关系,此前苹果与富士康将生产线向越南和印度转移让人认为苹果在减少对中国制造的依赖,然而 ...

  6. 用网站代替p2p服务器,[视频]PURSUIT:互联网可摆脱对服务器的依赖 用P2P取代

    截止今天,在线数据都是被存储于全世界不同地点的服务器上.如PC.平板或智能手机等客户端设备,会向地理上距离最近的服务器请求数据,以便让信息交流得更快. 但是这么做也有多个明显的不足,那就是必须依赖于服 ...

  7. 百度正在摆脱广告营收依赖!AI云增长64%成最大推动力

    来源:量子位 广告收入仅增长了1%,但百度的美股股价却一度跳涨11%. 截至今晨收盘,百度股价收涨6.84%. 百度刚刚发布的2021 Q4财报显示,百度广告业务收入为191亿元,增长率同比下降2个百 ...

  8. 使用ASP.NET Abstractions增强ASP.NET应用程序的可测试性

    概述 在阅读本文之前,兄弟们请先注意两点: 我们现在谈的是传统ASP.NET应用程序的可测试性,而不是ASP.NET MVC应用程序的可测试性. 我们现在谈的是"增强",而不是说传 ...

  9. 华为手机多久可以摆脱美国技术依赖?任正非放出豪言!

    12月9日,华为心声社区公开了任正非接受加拿大<环球邮报>采访纪要.加拿大记者Nathan VanderKlippe问到华为手机需要多久就可以完全摆脱美国技术依赖时,任正非回应:明年就可以 ...

最新文章

  1. DataList自定义分页
  2. 038_Unicode对照表四
  3. 2003服务器系统密码忘记,服务器系统2003 设置密码
  4. matlab在命令行和脚本,MATLAB学习笔记—函数与脚本
  5. Metrics-Java版的指标度量工具
  6. UVA10912 Simple Minded Hashing【DP】
  7. 在Simulink中对S 函数进行参数传递的三种方法
  8. python文字转语音
  9. 动态规划(DP算法)详解
  10. 学计算机专业开学要买笔记本电脑吗,大一开学需要买电脑吗 大学开学电脑买什么好...
  11. 学编程有什么用?零基础小白可以学吗?
  12. apache评分表的意义_APACHE评分系统及评分表 -
  13. 【SAP】ABAP——币种金额转换
  14. ADI Blackfin DSP处理器-BF533的开发详解61:DSP控制ADXL345三轴加速度传感器-LCD(含源码)
  15. [宋史学习] 宋太宗评价
  16. uboot-Makefile学习(4)
  17. 【重要征稿】IEEE 2022年网络、计算机和通信国际学术研讨会(ISNCC2022)
  18. 华清远见-重庆中心-数据库阶段技术总结/个人总结
  19. 2020年1月2日 林大OJ习题 暴力枚举
  20. Adobe安装程序无法访问关键文件/目录,请尝试重新安装。(错误代码:41) 解决方法

热门文章

  1. JAVA 邮件发送工具类
  2. SQL Server 2012中的Contained Database尝试
  3. 高动态范围(HDR)
  4. TCP 的连接建立:采用三报文握手
  5. 基于阿里的Node全栈之路(二)阿里负载均衡的HTTPS优化方案
  6. Canny边缘检测原理及C#程序实现
  7. 2017-6-3 jQuery 事件 DOM操作
  8. ViewPager详解(一)——ViewPager的基本使用完整示例
  9. Spring 入门知识点笔记整理
  10. 在不重装XP系统,增加系统盘剩余空间