使用ASP.NET Abstractions增强ASP.NET应用程序的可测试性
概述
在閱讀本文之前,兄弟們請先注意兩點(diǎn):
- 我們現(xiàn)在談的是傳統(tǒng)ASP.NET應(yīng)用程序的可測試性,而不是ASP.NET MVC應(yīng)用程序的可測試性。
- 我們現(xiàn)在談的是“增強(qiáng)”,而不是說傳統(tǒng)ASP.NET應(yīng)用程序做不到良好的可測試性,一切皆在人為。
關(guān)于可測試性的重要性,老趙覺得已經(jīng)不需要再過多強(qiáng)調(diào)了。如果您想要獲得高生產(chǎn)力,為代碼編寫單元測試似乎已經(jīng)是必經(jīng)之路了。不過可惜的是,ASP.NET應(yīng)用程序給人的感覺,始終是對可測試性不太友好,其最重要的原因之一在于對HttpContext對象的高度依賴,而我們很難對HttpContext編寫Mock或Stub:對于最常見的Mock框架來說,進(jìn)行Mock的方式在于對抽象類型進(jìn)行繼承和重寫,因此需要目標(biāo)類型必須能夠繼承,其成員也必須能夠重寫(override),可惜HttpContext對這兩個要求均不滿足——雖然我們有TypeMock這個強(qiáng)大的工具,只可惜它是商業(yè)產(chǎn)品。而且事實(shí)上,如果Moq等框架無法滿足您的要求,一般可以確定是設(shè)計(jì)有問題。從這個角度說,ASP.NET圍繞HttpContext開展的一系列功能,在設(shè)計(jì)上的確有不足之處。
因此,為了提高ASP.NET應(yīng)用程序的可測試性,各方都作了許多努力,其中的原則便是:盡可能減少對HttpContext的依賴(不可測試的邏輯),使邏輯依賴于特定的抽象類型。“特定”二字是指與您的業(yè)務(wù)或功能相關(guān)性,例如您在使用MVP模式進(jìn)行開發(fā)時,使用的每個類型都是領(lǐng)域相關(guān)(如User),或界面相關(guān)(如SelectList)的抽象類型,而不是具體的界面(如DropDownList)或協(xié)議(HttpContext1)相關(guān)類型。這往往需要您在具體類型上多加一個抽象層,針對抽象進(jìn)行編程。除了MVP模式之外,ASP.NET AJAX中的PageRequestManager也是如此,ScriptManager的各階段操作都簡單地委托給了PageRequestManager,這樣不可測試的邏輯(ScriptManager)減少了,可以測試的邏輯(PageRequestManager)增加了。
不過可以想到的是,圍繞HttpContext進(jìn)行編程的場景也是不可避免的,例如Http Handler/Module等ASP.NET基礎(chǔ)結(jié)構(gòu),亦或是連接HttpContext與抽象類型的“黏著劑”。關(guān)于這方面微軟也在改進(jìn),例如隨ASP.NET MVC發(fā)布了ASP.NET Abstraction,其中提供了抽象類型HttpContextBase(老趙個人不喜歡Base這樣的后綴,其實(shí)更喜歡IHttpContext這樣的接口類型),這是一個赤裸裸地抽象類,其中包含了HttpContext的所有成員,個個抽象。也正是由于這樣的抽象,使得圍繞HttpContext進(jìn)行單元測試的可行性大大增加了。當(dāng)然,這句話有個前提,那就是以前圍繞HttpContext編寫的代碼,現(xiàn)在要使用HttpContextBase了,這也是提高ASP.NET應(yīng)用程序可測試性的又一原則:對于一定要依賴HttpContext的邏輯,請依賴HttpContextBase。那么現(xiàn)在,兄弟們就隨老趙來看一下,如何使用ASP.NET Abstraction來輔助ASP.NET開發(fā)。
直接使用HttpContext進(jìn)行測試
HttpContext對象難以Mock,但是也并非說它的數(shù)據(jù)我們就無法“定制”,在某些“極端簡單”的情況下,我們還是可以直接構(gòu)造一個HttpContext對象進(jìn)行測試的。比如下面這個毫無意義的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的一個構(gòu)造函數(shù)創(chuàng)建對象,而這個構(gòu)造函數(shù)會接受一個HttpRequest和一個HttpResponse對象。HttpRequest對象構(gòu)造起來會接受文件名,路徑和Query String;而HttpResponse構(gòu)造時只需要一個TextWriter用于輸出信息。由于我們這個場景過于簡單,因此還真夠用了。代碼比較簡單,意義也很明確,就不多作解釋了。
不過很顯然,這種簡單場景是幾乎無法遇到的。如果我們需要POST的情況呢?做不到;如果我們需要設(shè)置UserAgent呢?做不到;如果我們要檢查Url Write的情況?做不到——統(tǒng)統(tǒng)做不到,真啥都別想做。因此我們還是無法使用這種方式進(jìn)行測試,這第一個例子僅僅是為了內(nèi)容“完整性”而加上的。
AuthorizedHandler
這個例子就復(fù)雜些了,并且直接來源于老趙以前的某個項(xiàng)目的代碼——當(dāng)然現(xiàn)在為了示例進(jìn)行了簡化和改造。在項(xiàng)目中我們往往要編寫一些Handler來處理客戶端的請求,而同時Handler需要對客戶端進(jìn)行身份驗(yàn)證及基于角色的授權(quán),只有特定角色的客戶才能訪問Handler的主體邏輯,否則便拋出異常。而這樣的邏輯有其固有的結(jié)構(gòu),因此我們這類Handler編寫一個公用的父類,這樣我們便可使用“模板方法”的形式來補(bǔ)充具體邏輯了。這個父類的實(shí)現(xiàn)如下:
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方法中進(jìn)行邏輯實(shí)現(xiàn),但是我們現(xiàn)在直接把方法調(diào)用轉(zhuǎn)發(fā)給接受HttpContextBase作為參數(shù)的ProcessRequest方法重載。HttpContextBase是一個抽象類型,這便是我們的測試目標(biāo)。這個方法首先判斷用戶是否經(jīng)過認(rèn)證,然后再將用戶的角色,與AuthorizedRoles抽象屬性中表示的合法角色進(jìn)行匹配,如果匹配成功則調(diào)用ProcessRequestCore抽象方法,而無論是用戶認(rèn)證還是授權(quán)失敗,都會拋出UnauthorizedAccessException異常。
這里有一個題外話:不知您是否注意到,這里沒有private方法,所有的方法都有internal修飾。這么做的原因完全是為了進(jìn)行單元測試。由于private方法無法被外部項(xiàng)目調(diào)用,因此我們只能使用internal作為修飾符,再為程序集加上InternalVisibleToAttribute標(biāo)記,把所有的internal成員向測試項(xiàng)目開放。當(dāng)然,此時程序集內(nèi)部就能夠隨意調(diào)用那些方法了——還好,都是自家人,注意點(diǎn)便是了。
這段邏輯需要測試的環(huán)節(jié)比較多,我們依次看一下:
[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); }這是對沒有通過身份驗(yàn)證的請求的回應(yīng),我們設(shè)置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();} }這是測試身份驗(yàn)證通過,而基于角色的授權(quán)失敗時的情況。我們把IsAuthenticated設(shè)為true,并且要求IsInRole方法在“接受到任何string類型參數(shù)”的時候都返回false,而最后再“象征性”地設(shè)置AuthorizedRoles所返回的內(nèi)容。這個測試的期望是拋出UnauthorizedAccessException,不過值得注意的是,我們的代碼還有其他要求,那就是要求IsInRole和AuthorizedRoles一定要調(diào)用過——您明白了嗎?這就是為什么對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(); }最后的測試自然是正常流程的測試。在這里我們要檢驗(yàn)的是正常情況下ProcessRequestCore是否“被調(diào)用,而且只被調(diào)用了一次”。如果您能夠理解前兩個測試,這個測試應(yīng)該也同樣簡單才是。
UrlRewriteModule
之前都是在測試Http Handler,不過Http Module的測試也較為類似。其原則是相同的:把所有邏輯轉(zhuǎn)發(fā)給針對抽象的方法。我們這次就以最最經(jīng)典的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本身會通過無參數(shù)的構(gòu)造函數(shù)進(jìn)行創(chuàng)建,這時就會使用默認(rèn)的RegexUrlRewriteSource對象。而在測試的時候,就要創(chuàng)建Mock對象并通過構(gòu)造函數(shù)的重載進(jìn)行“依賴注入”了。在Init方法中我們直接使用匿名委托來作為BeginRequest事件的處理函數(shù),而其中就把邏輯直接委托給TryRewritePath方法了。TryRewritePath方法會判斷Source中得知是否需要進(jìn)行URL重寫,并且在需要的時候調(diào)用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方法永遠(yuǎn)返回null,而此時也不應(yīng)該調(diào)用HttpContext的RewritePath方法。否則,便判斷給出合適的RawUrl和重寫目標(biāo),并判斷RewritePath方法有沒有正確調(diào)用過便是。其實(shí)單元測試就這么簡單。
結(jié)束
沒啥想說的,就這么結(jié)束吧。
您有什么想法嗎?說說看吧。
轉(zhuǎn)載于:https://www.cnblogs.com/JeffreyZhao/archive/2009/04/23/improve-asp-net-testability-via-abstractions.html
總結(jié)
以上是生活随笔為你收集整理的使用ASP.NET Abstractions增强ASP.NET应用程序的可测试性的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#几种常用的排序算法
- 下一篇: MyEclipse连接oracle9i: