控件中国网现已改版,您看到的是老版本网站的镜像,系统正在为您跳转到新网站首页,请稍候.......
中国最专业的商业控件资讯网产品咨询电话:023-67870900 023-67871946
产品咨询EMAIL:SALES@COMPONENTCN.COM

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

作者:老赵 出处:博客园 2010年01月27日 阅读:

概述

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

  • 我们现在谈的是传统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方法有没有正确调用过便是。其实单元测试就这么简单。(本文由控件中国网转载)

热推产品

  • ActiveReport... 强大的.NET报表设计、浏览、打印、转换控件,可以同时用于WindowsForms谀坔攀戀Forms平台下......
  • AnyChart AnyChart使你可以创建出绚丽的交互式的Flash和HTML5的图表和仪表控件。可以用于仪表盘的创......
首页 | 新闻中心 | 产品中心 | 技术文档 | 友情连接 | 关于磐岩 | 技术支持中心 | 联系我们 | 帮助中心 Copyright-2006 ComponentCN.com all rights reserved.重庆磐岩科技有限公司(控件中国网) 版权所有 电话:023 - 67870900 传真:023 - 67870270 产品咨询:sales@componentcn.com 渝ICP备12000264号 法律顾问:元炳律师事务所 重庆市江北区塔坪36号维丰创意绿苑A座28-5 邮编:400020
在线客服
在线客服系统
在线客服
在线客服系统