技术开发 频道

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

 【IT168技术文档】

转载自Jeff Zhao的博客

概述

 在阅读本文之前,兄弟们请先注意两点:

 我们现在谈的是传统ASP.NET应用程序的可测试性,而不是ASP.NET MVC应用程序的可测试性。

 我们现在谈的是“增强”,而不是说传统ASP.NET应用程序做不到良好的可测试性,一切皆在人为。

 关于可测试性的重要性,老赵觉得已经不需要再过多强调了。如果您想要获得高生产力,为代码编写单元测试似乎已经是必经之路了。不过可惜的是,ASP.NET应用程序给人的感觉,始终是对可测试性不太友好,其最重要的原因之一在于对HttpContext对象的高度依赖,而我们很难对HttpContext编写Mock或Stub:对于最常见的Mock框架来说,进行Mock的方式在于对抽象类型进行继承和重写,因此需要目标类型必须能够继承,其成员也必须能够重写(override),可惜HttpContext对这两个要求均不满足——虽然我们有TypeMock这个强大的工具,只可惜它是商业产品。而且事实上,如果Moq等框架无法满足您的要求,一般可以确定是设计有问题。从这个角度说,ASP.NET围绕HttpContext开展的一系列功能,在设计上的确有不足之处。

 因此,为了提高ASP.NET应用程序的可测试性,各方都作了许多努力,其中的原则便是:尽可能减少对HttpContext的依赖(不可测试的逻辑),使逻辑依赖于特定的抽象类型。“特定”二字是指与您的业务或功能相关性,例如您在使用MVP模式进行开发时,使用的每个类型都是领域相关(如User),或界面相关(如SelectList)的抽象类型,而不是具体的界面(如DropDownList)或协议(HttpContext1)相关类型。这往往需要您在具体类型上多加一个抽象层,针对抽象进行编程。除了MVP模式之外,ASP.NET AJAX中的PageRequestManager也是如此,ScriptManager的各阶段操作都简单地委托给了PageRequestManager,这样不可测试的逻辑(ScriptManager)减少了,可以测试的逻辑(PageRequestManager)增加了。

 不过可以想到的是,围绕HttpContext进行编程的场景也是不可避免的,例如Http Handler/Module等ASP.NET基础结构,亦或是连接HttpContext与抽象类型的“黏着剂”。关于这方面微软也在改进,例如随ASP.NET MVC发布了ASP.NET Abstraction,其中提供了抽象类型HttpContextBase(老赵个人不喜欢Base这样的后缀,其实更喜欢IHttpContext这样的接口类型),这是一个赤裸裸地抽象类,其中包含了HttpContext的所有成员,个个抽象。也正是由于这样的抽象,使得围绕HttpContext进行单元测试的可行性大大增加了。当然,这句话有个前提,那就是以前围绕HttpContext编写的代码,现在要使用HttpContextBase了,这也是提高ASP.NET应用程序可测试性的又一原则:对于一定要依赖HttpContext的逻辑,请依赖HttpContextBase。那么现在,兄弟们就随老赵来看一下,如何使用ASP.NET Abstraction来辅助ASP.NET开发。

 直接使用HttpContext进行测试

 HttpContext对象难以Mock,但是也并非说它的数据我们就无法“定制”,在某些“极端简单”的情况下,我们还是可以直接构造一个HttpContext对象进行测试的。比如下面这个毫无意义的Http Handler:

 public class CountDataHandler : IHttpHandler

 {

 public bool IsReusable { get { return true; } }

 public void ProcessRequest(HttpContext context)

 {

 string data = context.Request.QueryString["data"];

 if (data == null)

 {

 throw new ArgumentNullException("data");

 }

 context.Response.Write(data.Length);

 }

 }

 从Query String里获得data字段,如果没有该字段则抛出异常,如果有就输出它的长度。这个Handler的作用就是这么无聊,只是为了做一个简单的示例。那么对它的单元测试该怎么做呢?

 [TestMethod]

 [ExpectedException(typeof(ArgumentNullException))]

 public void ProcessRequestTest_Throw_ArgumentNullException_When_Data_Is_Empty()

 {

 HttpContext context = new HttpContext(

 new HttpRequest("test.aspx", "http://localhost/test.aspx", ""),

 new HttpResponse(new StringWriter()));

 CountDataHandler handler = new CountDataHandler();

 handler.ProcessRequest(context);

 }

 [TestMethod]

 public void ProcessRequestTest_Check_Output()

 {

 string data = "Hello World";

 TextWriter writer = new StringWriter();

 HttpContext context = new HttpContext(

 new HttpRequest(

 "test.aspx",

 "http://localhost/test.aspx",

 "data=" + HttpUtility.UrlEncode(data)),

 new HttpResponse(writer));

 CountDataHandler handler = new CountDataHandler();

 handler.ProcessRequest(context);

 Assert.AreEqual(data.Length.ToString(), writer.ToString(),

 "The output should be {0} but {1}.", data.Length, writer.ToString());

 }

 它的单元测试分两种情况,一是在data字段缺少的情况下需要抛出异常(ExpectedException),二便是正常的输出。在测试的时候,我们通过HttpContext的一个构造函数创建对象,而这个构造函数会接受一个HttpRequest和一个HttpResponse对象。HttpRequest对象构造起来会接受文件名,路径和Query String;而HttpResponse构造时只需要一个TextWriter用于输出信息。由于我们这个场景过于简单,因此还真够用了。代码比较简单,意义也很明确,就不多作解释了。

 不过很显然,这种简单场景是几乎无法遇到的。如果我们需要POST的情况呢?做不到;如果我们需要设置UserAgent呢?做不到;如果我们要检查Url Write的情况?做不到——统统做不到,真啥都别想做。因此我们还是无法使用这种方式进行测试,这第一个例子仅仅是为了内容“完整性”而加上的。

0
相关文章