情境:最近在測試一個.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的單元測試。步驟如下,在測試方法前面加上以下屬性
123 |
[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了。
1234567891011 |
[[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類;下面是一段簡單的代碼
1234 |
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()方法:
123456789101112131415 |
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的時候就是這樣子:
1234567 |
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好啊。