The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

ASP.NET MVC 單元測試系列 (6):測試 Routing 路由規則

有了前 5 篇的觀念支持,我們要開始對 ASP.NET MVC 專案的各個部分進行單元測試了,接下來的文章就會是一篇一篇的單元測試開發技巧,如果各位還有觀念上不清楚的地方,我會再用其他文章來解釋模稜兩可的觀念,尤其是介於「單元測試」與「整合測試」之間的灰色地帶或 TDD (測試導向開發) 等觀念,我想總要先讓各位先打從心裡願意開始撰寫單元測試比較重要吧!今天的文章會講解 ASP.NET MVC 的 Routing 路由規則如何進行單元測試。

我們知道在 ASP.NET MVC 裡的 Routing 有兩個主要的用途:

  • 瀏覽器傳來 HTTP 要求的 URL 會透過 Routing 比對出適當的 Controller / Action
    • 除了這個任務外,Routing 還肩負著決定網址格式與攜帶 RouteValue 等功用
  • 透過 Routing 路由規則定義產生適當的網址給瀏覽器
    • ASP.NET MVC 會依據 Routing 路由規則對應後產生一個最適合的網址
    • 在 ASP.NET MVC 內部都是由 RouteTable.Routes.GetVirtualPath 取得動態產生的網址

: ASP.NET Web Form 也可以用 Routing 來決定網址喔,並非是 ASP.NET MVC 的專利!

第一種:驗證瀏覽器傳來 HTTP 要求的 URL 會透過 Routing 比對出適當的 Controller / Action

我們再上一篇《ASP.NET MVC 單元測試系列 (5):瞭解 Stub 假物件》其實已經看到一個簡單的範例,我們利用 MvcContrib 提供的 FakeHttpContext 類別 ( Stub 類別 ) 幫我們建立出一個模擬的 HttpContext 物件,因此可以讓我們順利的完成 Routing 結果的測試。

在這篇文章裡,我們寫一個更上一篇文章不一樣的例子,雖然測試的方法與過程都一樣,但是程式碼稍有不同,我們省略了 RouteCollection 類別的建立並從 Arrange 區域中移除,而改以 RouteTable.Routes 屬性替代,為什麼在單元測試專案中可以使用 RouteTable.Routes 屬性呢?這不是在 ASP.NET MVC 的執行環境下才會有的東西嗎?

alt

那是因為 RouteTable.Routes 屬性是一個靜態屬性,而且 RouteTable 是一個靜態類別,而且在靜態類別的建構子裡就已經將 RouteTable.Routes 屬性所需的 _instance 靜態欄位設定好了,因此不管你是在哪種環境下執行,都可以直接使用 RouteTable.Routes 當成 RouteCollection 型別的物件來使用。

寫好之後我們先跑個測試:

alt

我們再撰寫另一個測試,這次我們要測試的是帶有 id 的 NewsDetail 動作方法,程式碼如下:

alt

因為幾乎是一樣的程式碼,所以我們再跑一次測試,不過竟然卻發生了錯誤!

alt

錯誤訊息是:

System.ArgumentException: 名為 'Default' 的路由已經在此路由集合中。路由名稱必須是唯一的。

我們在前幾篇系列文章有強調過:單元測試是軟體測試中的最小單位,必須是可信任的。以及我也有在《與 Roy Osherove 探討單元測試的藝術》文章裡整理出一句話:單元測試程式不應該接觸到任何與任何 外部資源 (External Resources) 或 靜態物件 (Static Objects)

我們這次寫的單元測試程式由於用到了一組 靜態物件 (RouteTable.Routes) 以致於我們的單元測試變的不可靠,如果你的測試程式讓你懷疑每一次的測試結果到底是不是對的,那麼你的單元測試就是失敗的。雖然我們第一個測試方法 (TestMethod) 可以正常執行測試,不過卻污染了 RouteTable.Routes 屬性的內容,導致後續的測試都會發生錯誤,我們稱這種狀況叫 Side Effect (副作用)。為了解決這個問題,我們可以選擇改用先前的寫法,或者改用以下技巧來免除靜態物件在各測試方法間互相影響的狀況。

我們可以在這個測試類別多新增一個方法,並套用 TestInitialize 屬性(Attribute),這個屬性在 Visual Studio Unit Testing Framework (Microsoft.VisualStudio.TestTools.UnitTesting 命名空間) 裡的用途是讓每一個 測試方法 (TestMethod) 在執行之前都會先執行的方法,我們可以利用這個特性讓測試方法執行前就先將 RouteTable.Routes 屬性的內容清空,這樣一來我們每一個測試方法間就不會再彼此互相影響了。

alt

此方法加上去之後,我們再執行一次測試,這時你就會發現全部測試成功了:

alt

接下來,我們來測試一個更複雜的 Routing 網址路由規則,我們先在 Global.asax.cs 加上以下規則:

alt

這個 Routing 規則有另外三個 Route Value 分別是 {year} 、 {month} 與 {day},除此之外我們還替這三個參數利用 Regular Expression 加上了格式的限制,只有符合這些條件限制才算比對成功,因此我們要在單元測試程式裡去驗證這些預期中的邏輯,程式如下:

alt

我想這段測試程式應該也非常直覺,因為我們輸入的網址 ( ~/News/Archive/2010/9/22/33945 ) 都是輸入正確的情況,都是屬於合理值的範圍,我們接下來要寫一些不合理的網址,以驗證我們的限制條件是否生效,因此我們再新增一個測試方法如下:

alt

由於我們「預期」不應該有任何路由比對成功,所以這時我們只要驗證最後取回的 routeData 是否為 null 即可判定是否有任何網址路由有比對成功,這時我們可以再跑一次所有測試看看是否都能正常執行:

alt

接下來,你可以試著修改 Global.asax.cs 裡 RegisterRoutes() 方法定義的網址路由規則,試著把限制移除後再跑一次測試看看,看是否如你預期的發生測試失敗的狀況!

alt

上面的幾個例子全部都是用「弱型別」的方式進行驗證,如果 Controller 類別或 Action 方法不存在的時候完全不會發出警告,也就是我們就真的只能用來測試 Routing 的定義到底跟我們預期的一不一樣而已。如果我們能用「強型別」的方式來驗證 Controller 或 Action 是否存在哪不就更加完美了,這等於多了一層保護機制,確保日後 Controller 與 Action 若更名也能一併在 Routing 的單元測試方法中一併檢查是否測試失敗。

要做到強型別的 Routing 單元測試,我們可以透過 MvcContrib 提供的 RouteTestingExtensions 類別,這裡定義了許多與測試 Routing 相關的擴充方法,可以讓你用更簡潔的方式開發 Routing 單元測試,以下是一個簡單的範例程式:

alt

透過 RouteTestingExtensions 的協助,我們連 FakeHttpContext 都不用建立,直接設定一個相對網址的字串就可以開始進行測試,測試的過程還會用強型別的方式一併檢查 Controller 類別與 Action 方法是否存在,不只是檢查 Controller 類別名稱與 Action 方法名稱而已,連呼叫 Action 所傳入的參數與方法都會一併做驗證,真的是專為 ASP.NET MVC 所設定一套類別。

另外在程式碼中還有特別用一個 try / catch 捕捉 AssertException 例外,因為 MvcContrib 的 TestHelper 組件並沒有與任何 Unit Test Framework 有任何牽連,所以利用 ShouldMapTo<T>() 方法或其他類似方法 ( TestHelper 組件中的其他擴充方法 ) 做測試驗證 (Assert) 時都是以引發例外 (Exception) 的方式來回報驗證成功或失敗,我們為了讓錯誤訊息可以更容易在「測試結果」視窗中直接顯示錯誤訊息,所以才會利用這個 try / catch 來包裝錯誤訊息,顯示錯誤的部分我們在下個例子看到。

如上程式範例的 ShouldMapTo<T>() 方法所傳入的是一個 Lamda Expression 語法,你除了可以直接模擬 (Mock) 執行 Action 的過程,所以連傳入參數、RouteValue 參數名稱、Action 方法參數名稱都會一併做驗證,我們可以用以下例子來說明:

我先在 HomeController 中新增一個 NewsDetail 方法並傳入一個名為 id 的參數:

alt

然後我在 Global.asax.cs 的 RegisterRoutes 方法加上以下路由規則:
: 第四個參數限制該 Rule 只有在使用 HTTP Method 為 GET 的情況下才會生效。

alt

最後我們來寫單元測試程式:

alt

當然,目前的一切測試當然都會通過,這時我們來實驗程式被改壞的情況,我們去 HomeController 修改 NewsDetail 的參數,把 id 修改成 news_id 就好:

alt

寫過 ASP.NET MVC 的人都知道這樣修改後一定要連帶修改 Global.asax.cs 中的 Routing 規則,否則無法透過 DefaultModelBinder 取得正確的 RouteValue,但我們先不修改 Global.asax.cs 中的 Routing 規則,直接執行測試程式,想當然爾當然會發生測試失敗的情況:

alt

這時我們去修改 Global.asax.cs 中的 Routing 規則,把原本的 {id} 修改成 {news_id} 即可:

alt

這時我們再執行一次測試程式,你將會發現測試就會成功了:

alt

有沒有發現這個 MvcContrib 提供的 TestHelper 組件有多麼好用,真是前人種樹、後人乘涼阿!

我想有了這些觀念,以後要測試各種複雜情況下的 Routing 應該都不是問題了。

 

第二種:透過 Routing 路由規則定義產生適當的網址給瀏覽器

測試由 RouteTable.Routes.GetVirtualPath 所產生的網址沒有那麼容易,在執行 GetVirtualPath 之前還需要額外準備一個 RequestContext 物件,該物件還必須傳入 HttpContextBase 與 RouteData 物件才能成功產生網址,若對 ASP.NET MVC 沒有一定程度瞭解的人可能不太容易看懂其來龍去脈。

透過 GetVirtualPath 產生網址必須知道當下的 HttpContext 資訊,這樣 ASP.NET MVC 才知道如何取得 Ambient Value ( 也就是目前的 Routing 資訊 ),這時再提供指定的 RouteValue 產生 VirtualPathData,這時才能透過 VirtualPathData.VirtualPath 取得動態產生的網址。

以下是單元測試的程式碼:

alt

國外幾乎找不到任何資訊有關利用 GetVirtualPath 產生網址的單元測試程式範例,光是寫出上述程式碼著實花了我不少時間,不過最終還是讓我給試出來了,測到七晚八晚才寫好,所以當別人還沒種樹的時候,我只好自己種了。

程式碼下載

相關連結

留言評論