使用ASP.NET Abstractions增强ASP.NET应用程序的可测试性
概述
在閱讀本文之前,兄弟們請先注意兩點:
- 我們現在談的是傳統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应用程序的可测试性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#几种常用的排序算法
- 下一篇: 学习 ASP.NET mvc 第一天、也