在C#单元测试中使用HttpContext的简单解决办法
场景:最近在测试一个.NET的Http Module,这个Module是用来做URL重写的。刚开始进展的比较顺利,因为该Module里面的方法参数基本上都是String,后来这个Module进行了一下重构,所有参数都变成了HttpContext了,这就直接导致原来的单元测试都跑不起来了,接着就开始了弄HttpContext了。
1. 采用Visual Studio自带的ASP.NET单元测试
刚开始我看了一下被测试的代码,虽然说用到了HttpContext,但是有很多地方我都可以绕过去的,意思就是这个HttpContext只是名以上需要的一个参数,只要它不是NULL就可以了,并不影响我的测试,所以我采用了ASP.NET Unit Test的办法来获取一个HttpContext,这个方法实现起来是最简单的,但是会有一些问题,后面会提及到。
首先新建一个WEB项目,然后把被测的Http Module挂到这个新建的WEB项目中。然后就可以配置ASP.NET单元测试,把原来一般的单元测试改造成为一个ASP.NET的单元测试。步骤如下,在测试方法前面加上以下属性
1 2 3 |
[HostType("ASP.NET")] [UrlToTest("http://localhost:6988/Default.aspx")] [AspNetDevelopmentServerHost("D:\\MS_Code\\Public\\UrlRouter.WebTest", "/")] |
[UrlToTest] — 这个属性指定了运行该单元测试时的URL
[HostType] — 一般的单元测试是在VSTest的宿主进程下运行,所以是没有HttpContext的;如果作为ASP.NET单元测试,那么必须要在ASP.NET宿主进程下运行
[AspNetDevelopmentServerHost] — 由于我使用了ASP.NET Development Server 作为测试的主机服务器,而不是IIS;所以我需要设置这个属性来指定Web应用程序的完整路径。这里我需要把目录指向刚才新建的WEB项目的路径中。这样子就可以在单元测试中直接使用HttpContext.Current来获取一个HttpContext了。
1 2 3 4 5 6 7 8 9 10 11 |
[[TestMethod] [HostType("ASP.NET")] [AspNetDevelopmentServerHost("D:\\MS_Code\\Public\\UrlRouter.WebTest", "/")] [UrlToTest("http://localhost:6988/Default.aspx")] public void Fuseaction_PUT() { handler = new UrlMapHandler(HttpContext.Current); Assert.IsNotNull(handler); result = handler.ExecuteUrlMap(); StringAssert.Contains(result.Url, "expected string"); } |
这样子初步解决了需要HttpContext的困难,但是会有以下问题:
- 对HttpContext的内容不可控
- 需要运行ASP.NET Development Server
- 不能进行调试
2. 直接创建HttpContext实例
基于刚才提到的3个不足,我决定直接创建一个HttpContext来解决问题。还好,HttpContext有公开的构造函数,这个构造函数需要接受一个HttpWorkerRequest作为参数,HttpWorkerRequest是一个抽象类,就是不能直接对之实例化啦,不过好消息是,微软有一个简单的类“SimpleWorkerRequest”,这个类实现了HttpWorkerRequest类;下面是一段简单的代码
1 2 3 4 |
TextWriter tw = new StringWriter(); HttpWorkerRequest wr = new SimpleWorkerRequest ("default.aspx", "friendId=1300000000", tw); HttpContext.Current = new HttpContext(wr); |
这样子就能在单元测试里面用到HttpContext了,好处有:
- 可以控制HttpContext的内容了,对于URL重写这部分来说,会比较关心请求路径,还有queryString,这两个可以在实例化SimpleWorkerRequest的时候,作为参数传递进去。
- 不需要ASP.NET Development Server。这个很重要,因为可以满足一个自包含(self-contained)的单元测试的要求
- 可以调试
3.改良后的方案
到此,世界很美好,测试进行的很顺利。好日子没过多久,我又遇到一个问题,这个URL重写模块,还应用了一个规则引擎,其中进行了大量对HOST判断的操作,很不幸,用刚才的方法创建的HttpContext,它返回的Request.Url中,HOST永远都是127.0.0.1,很郁闷。这时候,一个所有做.NET的人都需要的屠龙刀派上用场了!.NET Reflector,用Reflector打开System.Web.dll,找到System.Web.Hosting这个命名空间,然后找到SimpleWorkerRequest这个类,发现里面的GetLocalAddress()方法,发现里面就是Hard-Code了一个地址:127.0.0.1;好了,知道问题在哪里了,着手写一个新的类,这个类继承SimpleWorkerRequest,然后重写他的GetLocalAddress()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class MyWorkerRequest : SimpleWorkerRequest { private string localAdd = string.Empty; public MyWorkerRequest(string page, string query, TextWriter output, string address) : base(page, query, output) { this.localAdd = address; } public override string GetLocalAddress() { return this.localAdd; } } |
这样子,实例化HttpContext的时候就是这样子:
1 2 3 4 5 6 7 |
Thread.GetDomain().SetData(".appPath", "c:\\inetpub\\wwwroot\\webapp\\"); Thread.GetDomain().SetData(".appVPath", "/"); TextWriter tw = new StringWriter(); String address = "home.myspace.cn"; HttpWorkerRequest wr = new MyWorkerRequest ("default.aspx", "friendId=1300000000", tw, address); HttpContext.Current = new HttpContext(wr); |
需要注意的是,对于.appPath和.appVPath的设置是必须的,因为在SimpleWorkerRequest的构造函数中,会取这两个数值。
经过这样的改造,基本上已经满足了测试的需求了。
总结一下:
- 这个事情居然用了1天多时间,后来发现解决的办法就在我订阅的博客里面就有,以后遇到什么难题先找一个Google Reader。
- 多用.NET Reflector,Visual Studio经常让你看meta data,那些meta data对我们来说一点用处都没有,还是Reflector好啊。