概述

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

  • 我们现在谈的是传统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的情况?做不到——统统做不到,真啥都别想做。因此我们还是无法使用这种方式进行测试,这第一个例子仅仅是为了内容“完整性”而加上的。

AuthorizedHandler

这个例子就复杂些了,并且直接来源于老赵以前的某个项目的代码——当然现在为了示例进行了简化和改造。在项目中我们往往要编写一些Handler来处理客户端的请求,而同时Handler需要对客户端进行身份验证及基于角色的授权,只有特定角色的客户才能访问Handler的主体逻辑,否则便抛出异常。而这样的逻辑有其固有的结构,因此我们这类Handler编写一个公用的父类,这样我们便可使用“模板方法”的形式来补充具体逻辑了。这个父类的实现如下:

public abstract class AuthorizedHandler : IHttpHandler
{public bool IsReusable { get { return false; } }void IHttpHandler.ProcessRequest(HttpContext context){this.ProcessRequest(new HttpContextWrapper(context));}internal void ProcessRequest(HttpContextBase context){if (!context.User.Identity.IsAuthenticated){throw new UnauthorizedAccessException();}foreach (var role in this.AuthorizedRoles){if (context.User.IsInRole(role)){this.ProcessRequestCore(context);return;}}throw new UnauthorizedAccessException();}protected internal abstract void ProcessRequestCore(HttpContextBase context);protected internal abstract IEnumerable<string> AuthorizedRoles { get; }
}

一般来说,我们会在IHttpHandler.ProcessRequest方法中进行逻辑实现,但是我们现在直接把方法调用转发给接受HttpContextBase作为参数的ProcessRequest方法重载。HttpContextBase是一个抽象类型,这便是我们的测试目标。这个方法首先判断用户是否经过认证,然后再将用户的角色,与AuthorizedRoles抽象属性中表示的合法角色进行匹配,如果匹配成功则调用ProcessRequestCore抽象方法,而无论是用户认证还是授权失败,都会抛出UnauthorizedAccessException异常。

这里有一个题外话:不知您是否注意到,这里没有private方法,所有的方法都有internal修饰。这么做的原因完全是为了进行单元测试。由于private方法无法被外部项目调用,因此我们只能使用internal作为修饰符,再为程序集加上InternalVisibleToAttribute标记,把所有的internal成员向测试项目开放。当然,此时程序集内部就能够随意调用那些方法了——还好,都是自家人,注意点便是了。

这段逻辑需要测试的环节比较多,我们依次看一下:

[TestMethod()]
[ExpectedException(typeof(UnauthorizedAccessException))]
public void ProcessRequestTest_Nonauthenticated_Request()
{Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(MockBehavior.Strict);mockContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(false);Mock<AuthorizedHandler> mockHandler = new Mock<AuthorizedHandler> { CallBase = true };mockHandler.Setup(h => h.ProcessRequestCore(It.IsAny<HttpContextBase>())).Throws(new Exception("ProcessRequestCore should not be called."));mockHandler.Setup(h => h.AuthorizedRoles).Throws(new Exception("AuthorizedRoles should not be accessed."));mockHandler.Object.ProcessRequest(mockContext.Object);
}

这是对没有通过身份验证的请求的回应,我们设置HttpContext.User.Identity.IsAuthenticated属性为false,并且声明不能碰触到ProcessRequestCore和AuthroizedRoles属性。在这样的情况下,我们自然期望抛出UnauthorizedAccessException。

[TestMethod()]
[ExpectedException(typeof(UnauthorizedAccessException))]
public void ProcessRequestTest_Nonauthorized_Request()
{Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(MockBehavior.Strict);mockContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true);mockContext.Setup(c => c.User.IsInRole(It.IsAny<string>())).Returns(false).Verifiable();Mock<AuthorizedHandler> mockHandler = new Mock<AuthorizedHandler> { CallBase = true };mockHandler.Setup(c => c.ProcessRequestCore(It.IsAny<HttpContextBase>())).Throws(new Exception("ProcessRequestCore should not be called."));mockHandler.Setup(c => c.AuthorizedRoles).Returns(new string[] { "admin", "user" }).Verifiable();try{mockHandler.Object.ProcessRequest(mockContext.Object);}catch{throw;}finally{mockContext.Verify();mockHandler.Verify();}
}

这是测试身份验证通过,而基于角色的授权失败时的情况。我们把IsAuthenticated设为true,并且要求IsInRole方法在“接受到任何string类型参数”的时候都返回false,而最后再“象征性”地设置AuthorizedRoles所返回的内容。这个测试的期望是抛出UnauthorizedAccessException,不过值得注意的是,我们的代码还有其他要求,那就是要求IsInRole和AuthorizedRoles一定要调用过——您明白了吗?这就是为什么对Mock对象追加Verifiable和Verify方法,并且使用try/catch/finally的缘故。

[TestMethod()]
public void ProcessRequestTest_Authorized_Request()
{Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(MockBehavior.Strict);mockContext.Setup(c => c.User.Identity.IsAuthenticated).Returns(true);mockContext.Setup(c => c.User.IsInRole(It.IsAny<string>())).Returns(false);mockContext.Setup(c => c.User.IsInRole("user")).Returns(true).Verifiable();Mock<AuthorizedHandler> mockHandler = new Mock<AuthorizedHandler> { CallBase = true };mockHandler.Setup(c => c.ProcessRequestCore(It.IsAny<HttpContextBase>())).AtMostOnce().Verifiable();mockHandler.Setup(c => c.AuthorizedRoles).Returns(new string[] { "admin", "user" }).Verifiable();mockHandler.Object.ProcessRequest(mockContext.Object);mockHandler.Verify();mockContext.Verify();
}

最后的测试自然是正常流程的测试。在这里我们要检验的是正常情况下ProcessRequestCore是否“被调用,而且只被调用了一次”。如果您能够理解前两个测试,这个测试应该也同样简单才是。

UrlRewriteModule

之前都是在测试Http Handler,不过Http Module的测试也较为类似。其原则是相同的:把所有逻辑转发给针对抽象的方法。我们这次就以最最经典的URL重写功能为例,如下:

public interface IUrlRewriteSource
{string GetRewritePath(string rawUrl);
}public class UrlRewriteModule : IHttpModule
{public void Dispose() { }public UrlRewriteModule(): this(new RegexUrlRewriteSource(...)){ }internal UrlRewriteModule(IUrlRewriteSource source){this.m_source = source;}private IUrlRewriteSource m_source;public void Init(HttpApplication httpApp){httpApp.BeginRequest += (sender, e) =>{HttpContext context = ((HttpApplication)sender).Context;this.TryRewritePath(new HttpContextWrapper(context));};}internal void TryRewritePath(HttpContextBase context){string newUrl = this.m_source.GetRewritePath(context.Request.RawUrl);if (!String.IsNullOrEmpty(newUrl)){context.RewritePath(newUrl);}}
}

由于测试需要,我们提取出一个IUrlRewriteSource接口。ASP.NET本身会通过无参数的构造函数进行创建,这时就会使用默认的RegexUrlRewriteSource对象。而在测试的时候,就要创建Mock对象并通过构造函数的重载进行“依赖注入”了。在Init方法中我们直接使用匿名委托来作为BeginRequest事件的处理函数,而其中就把逻辑直接委托给TryRewritePath方法了。TryRewritePath方法会判断Source中得知是否需要进行URL重写,并且在需要的时候调用RewritePath方法。它的测试如下:

[TestMethod]
public void TryRewritePathTest_No_Rewrite()
{Mock<IUrlRewriteSource> mockSource = new Mock<IUrlRewriteSource>();mockSource.Setup(s => s.GetRewritePath(It.IsAny<string>())).Returns<string>(null).Verifiable();Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(MockBehavior.Strict);mockContext.Setup(c => c.Request.RawUrl).Returns("Hello");mockContext.Setup(c => c.RewritePath(It.IsAny<string>())).Throws(new InvalidOperationException("Should not call the RewritePath method."));UrlRewriteModule module = new UrlRewriteModule(mockSource.Object);module.TryRewritePath(mockContext.Object);mockSource.Verify();
}[TestMethod]
public void TryRewritePathTest_Rewrite_Article_Detail_Page()
{string rawUrl = "Article/5";string targetUrl = "~/Article.aspx?id=5";Mock<IUrlRewriteSource> mockSource = new Mock<IUrlRewriteSource>();mockSource.Setup(s => s.GetRewritePath(It.IsAny<string>())).Throws(new InvalidOperationException("Why so many unnecessary method calls?"));mockSource.Setup(s => s.GetRewritePath(rawUrl)).Returns(targetUrl).Verifiable();Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>(MockBehavior.Strict);mockContext.Setup(c => c.Request.RawUrl).Returns(rawUrl);mockContext.Setup(c => c.RewritePath(targetUrl)).Verifiable();UrlRewriteModule module = new UrlRewriteModule(mockSource.Object);module.TryRewritePath(mockContext.Object);mockSource.Verify();mockContext.Verify();
}

在不需要重写的情况下,IUrlRewriteSource对象的GetRewritePath方法永远返回null,而此时也不应该调用HttpContext的RewritePath方法。否则,便判断给出合适的RawUrl和重写目标,并判断RewritePath方法有没有正确调用过便是。其实单元测试就这么简单。

结束

没啥想说的,就这么结束吧。

您有什么想法吗?说说看吧。

转载于:https://www.cnblogs.com/JeffreyZhao/archive/2009/04/23/improve-asp-net-testability-via-abstractions.html

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

  1. IIS 7.0: 使用集成的 ASP.NET 管道增强应用程序

    IIS 7.0 使用集成的 ASP.NET 管道增强应用程序 Mike Volodarsky 代码下载位置: PHPandIIS2008_01.exe (171 KB) Browse the Code ...

  2. ASP基础教程:ASP脚本变量、函数、过程和条件语句

    在上一期中作者向诸位简要介绍了 ASP 脚本语言之一 VBScript 的一些基本常识,本期将继续给大家讲解 VBScript 的脚本编写方法,并通过展示 VBScript 在 ASP 程序编写过程中 ...

  3. 加快ASP。NET Core WEB API应用程序。第3部分

    下载source from GitHub 对ASP进行深度重构和优化.NET Core WEB API应用程序代码 介绍 第1部分.创建一个测试的RESTful WEB API应用程序. 第2部分.增 ...

  4. asp php 语法区别,asp与php语法对比

    下面给大家介绍一些php和asp语法上的区别: 1.定界符 PHP的定界符是:php 开始和结束<?php ?>,行尾有:号 ASP的定界符是    asp 开始和结束 2.大小写区分 P ...

  5. asp.net编程:asp.net中如何设置页面的编码

    在用ASP.NET写网上支付的接口程序时,遇到一个奇怪问题,通过表单提交过去的中文全是乱码,英文正常.而用asp程序进行测试,可以正常提交 中 文,asp页面中有这样的HTML代码: < met ...

  6. 返璞归真 asp.net mvc (10) - asp.net mvc 4.0 新特性之 Web API

    返璞归真 asp.net mvc (10) - asp.net mvc 4.0 新特性之 Web API 原文:返璞归真 asp.net mvc (10) - asp.net mvc 4.0 新特性之 ...

  7. 七天来学习ASP.NET MVC (两)——ASP.NET MVC 数据传输

    通过第一天的学习之后,我们相信您已经对MVC有一些基本了解. 本节所讲的内容是在上节的基础之上.因此须要确保您是否掌握了上一节的内容. 本章的目标是在今天学习结束时利用最佳实践解决方式创建一个小型的M ...

  8. asp.net权限设置可能导致应用程序无法正常运行(转)

    asp.net权限设置可能导致应用程序无法正常运行   有些时候我们写的asp.net应用程序是运行在虚拟主机上.有一些虚拟主机可能是由于安全的考虑,对asp.net做了权限设置,会导致我们的应用程序 ...

  9. [ASP.NET MVC2 系列] ASP.NET MVC 之如何创建自定义路由约束

     [ASP.NET MVC2 系列]      [ASP.NET MVC2 系列] ASP.Net MVC教程之<在15分钟内用ASP.Net MVC创建一个电影数据库应用程序>      ...

最新文章

  1. 21个令程序员泪流满面的瞬间
  2. 复习--3--对于第三堂课的总结--将两个页面相互用超链接链接到一起
  3. c#.net课程设计:ZCMU通讯录(待更新)
  4. chrome麦克风权限_如何在Chrome扩展程序中处理麦克风输入权限和语音识别
  5. 基于改进YOLO v3网络的夜间环境柑橘识别方法
  6. eclipse快捷键_Eclipse快捷键
  7. c# mvc如何生成excel
  8. 通过CN3口直接控制台达伺服电机A2-M(二)
  9. 怎么样利用“消息集中管控中心”批量管理手机信息
  10. android 手机上设置呼叫转移
  11. Unity 资源断舍离(资源清理重复以及引用被引用查找)
  12. 【翻唱】学习日语歌 (青鸟)火影忍者 OP
  13. 中药学(综合练习)题库【1】
  14. 成龙坦言演蒲松龄曾打退堂鼓:我演大文豪谁信啊
  15. java百度地图离线LBS_Web版百度地图加载离线瓦片
  16. 多ip服务器代理设置
  17. 通过推送消息控制Android系统锁屏、唤醒
  18. 每个男孩的机械梦「GitHub 热点速览 v.21.41」
  19. C compiler cc is not found
  20. Ubunto 常见操作

热门文章

  1. linux kernel 2.6.36 编译升级
  2. js基础-字符串常用属性合集
  3. 列表表格以及媒体元素
  4. 【51NOD1287】加农炮
  5. phpexcel用法(转)
  6. 貌似长沙有个用膳吧外卖网
  7. Net设计模式实例之解释器模式(Interpreter Pattern)
  8. Hadoop参数汇总
  9. POJ2337 欧拉路径字典序输出
  10. UVA11462年龄排序