天天看點

艾偉:盡可能擺脫對HttpContext的依賴

  在ASP.NET中進行單元測試的天敵便是HttpContext,它是ASP.NET的核心,極端複雜,卻無法進行Mock1——可見微軟能夠寫出那麼龐大的ASP.NET架構真不那麼容易。現在這個狀況改善了不少,是以大家已經可以使用System.Web.Abstractions.dll了,這個程式集中提供了對于HttpContext的抽象,也就是HttpContextBase抽象類。是以在ASP.NET MVC中,各種元件均依賴于HttpContextBase而不是HttpContext。這是一個優秀的做法,大家以後可以盡可能地擺脫HttpContext了。

  不過這似乎又是一個悖論。雖然已經可以對HttpContext進行Mock(這點增強了可測試性),但是過度依賴HttpContext對于單元測試來說也是一個傷害。這是HttpContext對象的天性所緻:它實在太複雜了。您應該已經察覺到,這是個集萬千寵愛于一身的對象,從請求,回複,應用程式,緩存……幾乎包含了Web應用程式需要的所有資訊。如果要測試一個依賴于HttpContext的方法,您勢必要為HttpContext的Mock對象填充各種資訊——其複雜程度視業務而定。而且,Mock關注的是“行為”,也就是說它關注的是做一件事情所使用“路徑”。那麼如果做一件事情可以采用多個路徑又會怎樣?是否需要在測試之前準備好所有的路徑,并且驗證被測試的代碼“采用了,并僅僅采用了其中一條路徑”?是以,Stub慢慢進入人們的視線。Stub關注的是“狀态”……這就是另一個話題了,還會涉及到采用Record & Replay還是Arrange-Act-Assert方式來進行單元測試,暫且不提。

  在ASP.NET MVC中負責“轉化資料”的層次為Model Binder。關于這一點,現有的“示例”大都關注把Form或QueryString中的資料轉化為Action參數上,不過Model Binder可用的地方其實更多。例如在《最佳實踐》的代碼中,原本AccountController的Delete方法實作如下:

<a href="http://11011.net/software/vspaste"></a>

  在删除了指定對象之後,頁面将跳轉到Url Referrer位址中。在上面的代碼中,這個值将通過通路Request.UrlReferer來獲得。這就使您的Action方法與HttpContext産生了依賴,是以它的單元測試代碼就需要這樣編寫:

  在單元測試代碼中,我們Mock了一個HttpContextBase對象,讓它的Request.UrlReferrer屬性傳回我們準備好的對象,再構造一個新的ControllerContext并交給Controller。而如果我們的UrlReferrer能夠作為Delete方法的參數,那麼單元測試代碼就會一下子簡單很多:

  有些朋友可能會問,不就是從Request的UrlReferrer屬性中取值嗎?我們為什麼要構造一個ControllerContext,不能直接設定Controller對象嗎?例如這樣就簡單多了:

  似乎可行,不過您運作的時候就會發現,架構會抛出異常,說隻有接口的成員,或可以override的成員才能夠被Mock。沒錯,Controller的Request屬性不是virtual的,無法override。Controller類如此設計是故意的,目的就是限制了可用的路徑。試想,如果您Mock了Controller.Request屬性,但是程式代碼通過Controller.HttpContext.Request進行通路又怎麼辦呢?類似的做法還有對方法重載的設計。一般來說,都會把其中幾個方法委托給其中唯一的方法,而隻有那個方法是可以被override的。這樣在編寫測試時,我們僅有的Mock入口便确定了,避免了測試代碼過度了解方法實作的問題。

  回到正題。如果要讓Delete方法接urlReferrer受參數,那麼我們就要編寫Model Binder相關的元件:

  并使其可以直接運用到Action的參數上:

  于是乎,我們的Delete方法便可寫為:

  如今的代碼,無論是應用程式還是架構類庫,都必須考慮“可測試性”這個要求。例如.NET 3.0的WF,由于其可測試性不佳一直為人所诟病。現在我們在編寫程式時,要時刻詢問自己:“這麼做友善測試嗎?”考慮到這個問題,可能您就會放心地做出某些抉擇了2。

  注1:其實還是可以Mock的。例如Typemock使用Profiler的方式進行直接注入,可以Mock任何成員。不過,如果Moq等架構無法滿足您的需要,一般便是您的設計有些問題了。

  注2:例如,究竟讓Action方法傳回ActionResult,還是傳回void,并直接通過Response輸出呢?

繼續閱讀