場景:最近在測試一個.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好啊。