﻿# 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](http://msdn.microsoft.com/en-us/library/system.web.routing.routecollection.getvirtualpath.aspx) 取得動態產生的網址

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

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

我們再上一篇《[ASP.NET MVC 單元測試系列 (5)：瞭解 Stub 假物件](/post/2010/09/17/ASPNET-MVC-Unit-Testing-Part-05-Using-Stub-Object)》其實已經看到一個簡單的範例，我們利用 [MvcContrib](https://github.com/MvcContrib/MvcContrib) 提供的 FakeHttpContext 類別 ( Stub 類別 ) 幫我們建立出一個模擬的 HttpContext 物件，因此可以讓我們順利的完成 Routing 結果的測試。

在這篇文章裡，我們寫一個更上一篇文章不一樣的例子，雖然測試的方法與過程都一樣，但是程式碼稍有不同，我們省略了 [RouteCollection](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routecollection.aspx) 類別的建立並從 Arrange 區域中移除，而改以 [RouteTable.Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 屬性替代，為什麼在單元測試專案中可以使用 [RouteTable.Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 屬性呢？這不是在 ASP.NET MVC 的執行環境下才會有的東西嗎？

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/3ad4890425f00aaac8c6-image_214.png "title")

那是因為 [RouteTable.Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 屬性是一個靜態屬性，而且 [RouteTable](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.aspx) 是一個靜態類別，而且在靜態類別的建構子裡就已經將 [RouteTable.Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 屬性所需的 \_instance 靜態欄位設定好了，因此不管你是在哪種環境下執行，都可以直接使用 [RouteTable.Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 當成 [RouteCollection](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routecollection.aspx) 型別的物件來使用。

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/9bd3e5e81c2832f194ab-image_215.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/d4cfdcdd7a2c49f68eac-image_216.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/d0216d7cad6e031ab775-image_217.png "title")

**錯誤訊息是：**

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

我們在前幾篇系列文章有強調過：**單元測試**是軟體測試中的**最小單位**，必須是**可信任的**。以及我也有在《[與 Roy Osherove 探討單元測試的藝術](/post/2010/02/21/The-Art-of-Unit-Testing-with-Roy-Osherove-Notes.aspx)》文章裡整理出一句話：單元測試程式不應該接觸到任何與任何 **外部資源** (External Resources) 或 **靜態物件** (Static Objects)。

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

我們可以在這個測試類別多新增一個方法，並套用 [TestInitialize](http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.testtools.unittesting.testinitializeattribute.aspx) 屬性(Attribute)，這個屬性在 [Visual Studio Unit Testing Framework](http://msdn.microsoft.com/en-us/library/ms243147\(VS.80\).aspx) ([Microsoft.VisualStudio.TestTools.UnitTesting 命名空間](http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.testtools.unittesting.aspx)) 裡的用途是讓每一個 **測試方法** (TestMethod) 在執行之前都會先執行的方法，我們可以利用這個特性讓測試方法執行前就先將 [RouteTable.Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 屬性的內容清空，這樣一來我們每一個測試方法間就不會再彼此互相影響了。

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/3e304da85b711fb147a0-image_218.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/cc1118cbb32e0c69144f-image_219.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/12bc037410de90974efe-image_220.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/544dca3363a8a63ad185-image_221.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/bf402954977fef587563-image_222.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/5f5e7be82507a36a9fc9-image_223.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/5defc3bd40059a996fe6-image_224.png "title")

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

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/b29ee6fbdde18c660ea2-image_225.png "title")

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

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

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

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/37e3d836227d5a8d964f-image_226.png "title")

然後我在 Global.asax.cs 的 RegisterRoutes 方法加上以下路由規則：  
**註**: [第四個參數](http://msdn.microsoft.com/zh-tw/library/system.web.routing.route.constraints.aspx)限制該 Rule 只有在使用 HTTP Method 為 GET 的情況下才會生效。

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/3101b2850b6cb6dd3a78-image_227.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/0706a8a25148837f9cd7-image_228.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/cc3c622d3c7a338f1f68-image_229.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/cd21284507aa35e42279-image_230.png "title")

這時我們去修改 Global.asax.cs 中的 Routing 規則，把原本的 {id} 修改成 {news\_id} 即可：

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/a0785ed01911809a04d8-image_231.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/6bc14b348cdde857f731-image_232.png "title")

有沒有發現這個 [MvcContrib](https://github.com/MvcContrib/MvcContrib) 提供的 TestHelper 組件有多麼好用，真是前人種樹、後人乘涼阿！

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

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

測試由 [RouteTable.Routes.GetVirtualPath](http://msdn.microsoft.com/en-us/library/system.web.routing.routecollection.getvirtualpath.aspx) 所產生的網址沒有那麼容易，在執行 GetVirtualPath 之前還需要額外準備一個 RequestContext 物件，該物件還必須傳入 HttpContextBase 與 RouteData 物件才能成功產生網址，若對 ASP.NET MVC 沒有一定程度瞭解的人可能不太容易看懂其來龍去脈。

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

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/23400c3863da5e2afbc2-image_233.png "title")

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

**程式碼下載**

-   [MvcApplication6.zip](http://cid-ce50199d9432ea57.office.live.com/self.aspx/公開/ASP.NET%20MVC%20單元測試系列/MvcApplication6.zip)

**相關連結**

-   [與 Roy Osherove 探討單元測試的藝術 (心得筆記)](/post/2010/02/21/The-Art-of-Unit-Testing-with-Roy-Osherove-Notes.aspx)
-   [Testing Routes In ASP.NET MVC](http://haacked.com/archive/2007/12/17/testing-routes-in-asp.net-mvc.aspx)
-   [ASP.NET Routing](http://msdn.microsoft.com/en-us/library/cc668201.aspx) / [ASP.NET 路由](http://msdn.microsoft.com/zh-tw/library/cc668201\(VS.90\).aspx)
-   [ASP.NET MVC Framework (Part 2): URL Routing](https://weblogs.asp.net/scottgu/asp-net-mvc-framework-part-2-url-routing)
-   [Stephen Walther on ASP.NET MVC - Tips](https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/)

-   [ASP.NET MVC Tip #12 – Faking the Controller Context](https://web.archive.org/web/20120504105013/http://stephenwalther.com:80/blog/archive/2008/07/01/asp-net-mvc-tip-12-faking-the-controller-context.aspx)
-   ASP.NET MVC Tip #13 – Unit Test Your Custom Routes

-   [Fluent Route Testing in ASP.NET MVC](https://web.archive.org/web/20101122152932/http://flux88.com/blog/fluent-route-testing-in-asp-net-mvc/)
-   [mvccontrib test helper and verifying http post routes and parameters](http://stackoverflow.com/questions/2159589/mvccontrib-test-helper-and-verifying-http-post-routes-and-parameters)
-   [MvcContrib: a Codeplex Foundation project](https://github.com/MvcContrib/MvcContrib)

-   [Test Helper](https://github.com/mvccontrib/MvcContrib-2/tree/master/src/MvcContrib.TestHelper)

-   [The .NET MVC Framework – Part 2 – Testing Complex Routes and Controllers](http://tiredblogger.wordpress.com/2007/12/18/the-net-mvc-framework-part-2-testing-complex-routes-and-controllers/)
-   [ASP.NET MVC 開發心得分享 (8)：Routing 設定注意事項](/post/2009/07/20/ASPNET-MVC-Developer-Note-Part-8-Routing.aspx)
-   [Visual Studio Team System - Unit Testing Framework](http://msdn.microsoft.com/en-us/library/ms243147.aspx)
-   [Using the Assert Classes](http://msdn.microsoft.com/en-us/library/ms182530.aspx)
-   [Microsoft.VisualStudio.TestTools.UnitTesting Namespace](http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.testtools.unittesting.aspx)
-   [ASP.NET MVC + MVC Contrib + Unit Testing](https://weblogs.asp.net/psteele/asp-net-mvc-mvc-contrib-unit-testing)
