前天的文章我介紹了 Mock 假物件 (Fake Object),今天要講另一種假物件稱為 Stub 假物件,這種假物件目的與用途都跟 Mock 非常相近,總之就是為了讓單元測試程式可以順利執行而生的一種開發方式,這兩種假物件類型在單元測試的領域都非常重要,而且各有各的存在必要性,在日後單元測試的日子裡都會經常用到這兩個東西。
我們使用 Mock 技術可以透過介面(Interface)直接無中生有的產生一個假物件,而這類物件可以讓你自由定義該物件應該怎麼執行、傳入什麼資料、回傳什麼預期結果,把物件當成傀儡在用。而 Stub 物件的差別就在於他是真的有個類別定義存在,而這個類別可以被實體化成一個真的物件來用,進而讓測試程式在執行時更能以接近真實的狀況模擬出測試環境 (例如模擬出 HttpContext 的種種物件狀態)。
我們在 ASP.NET MVC 撰寫單元測試,就必須在單元測試的環境下模擬出 ASP.NET MVC 真正執行時的狀態,像是 HttpContext、HttpRequest、HttpResponse、HttpSessionState、Identity、Principal 這些物件都是非常複雜的東西,且牽連的範圍非常廣泛,廣泛到光是撰寫 Mock 就可以耗盡你所有時間,每寫一個測試方法 (Test Method) 都要 Mock 模擬所有的可能結果。
假設你修改了一支之前寫過的 Action 動作方法,修改完後你發現你的單元測試程式執行時卻發生例外,因為你在 Action 動作方法裡用到了一個之前沒有 Mock 到的動作 ( 即執行一個方法 ),為了要讓程式可以正常執行,你又要去修改測試方法裡的 Mock 語法或新增一些 Mock 動作 ( Setup ),在開發單元測試的過程中久而久之你就會覺得你做了許多沒意義的事,因為我們在 Action 裡寫的那些程式碼根本不需要在「單元測試」裡面做任何測試,但卻得每次修改程式都連帶修改測試程式碼,那不是很煩人嗎?
沒錯!這就是 Stub 物件的主要目的,你可以實做某個常用的介面、繼承一般類別或抽象類別實做自己的類別,而這個 Stub 類別每個實做的方法 (Method) 都於原本的類別相容,如此一來,你就可以將此 Stub 類別所建立的物件實體(object instance)傳入要被測試的類別中,在這種情況下,你幾乎不需要再額外做出任何 Mock 的動作,因為這等於你騙被測試的對象他在執行的時候的確擁有而且可以使用這些物件,也因此你的 Stub 類別定義的越真實,相對的你的測試程式就會跑得越順利!
為了證明 Stub 假物件的優異之處,我寫了一個小範例讓大家感受一下,我們來測試在 Global.asax.cs 檔案裡的 RegisterRoutes 方法,用來驗證我們定義的網址路由到底有沒有如我們預期的在運作:
好了,你看到了這段 Code 請問你要如何開始撰寫單元測試呢?而這個 Method 就只有兩行而已,而且 IgnoreRoute 與 MapRoute 這兩個擴充方法都來自其他類別的方法,還記得上一篇文章提到的一句話嗎:「單元測試是軟體測試的最小單位,如果測試的範圍輕易的就會擴展到其他類別或同類別的其他方法,那就不再是最小單位,也就不是單元測試了!」那我們需要測試這兩個方法執行的正確性嗎?這個單元測試我們到底測的是 RegisterRoute、IgnoreRoute 還是 MapRoute 呢?
我們在 瞭解 Mock 假物件 這篇文章的第一個問題有提過一句話:「不應該測試 GetMemberByAccount() 方法執行的正確性!」這句話代表著兩個含意:
- 我們不去驗證這段程式的執行結果 ( 例如我們只需要判斷他真的有呼叫過某類別的特定方法 )
- 我們相信這段程式的執行結果一定是對的 ( 例如這已經是一個被完整測試過確認無誤的程式 )
以本篇文章的這個例子來說,我們要單元測試的對象是 RegisterRoute,這是是無庸置疑的,但是我們也需要讓 IgnoreRoute 與 MapRoute 真的去執行,而且我們相信這兩個方法的執行結果一定會如我們預期的回傳正確的結果,在這個前提之下或許有點大膽,不過如果你沒有這個前提,你根本無法進行測試,或是你只能進行無意義的測試,例如你只驗證在 RegisterRoute 方法裡必須要執行 MapRoute 方法,這也太瞎了吧!
以我們對 ASP.NET MVC 的瞭解,我們知道當瀏覽器發出 HTTP 要求並指定 URL 取得資源時會經過 Routing 這一段,而在執行過 RegisterRoute 方法後我們會得到一組 RouteData 資料,裡面包含了一堆 RouteValueDictionary 的集合資料,例如包括 controller、action 等等之類的路由值(RouteValue)。
這時問題來了,第一,我們不瞭解 IgnoreRoute 與 MapRoute 的實做細節,基於物件導向程式開發的開放封閉原則 (Open/closed principle;OCP) 我們也不應該去瞭解 IgnoreRoute 與 MapRoute 的實做細節,我們只要知道怎麼使用它就好了。第二,由於這是 Web 環境,我們要在單元測試環境下必須模擬出非常多可能的變數,事實上,這個變數就是 HttpContext 類別,但是這個類別又再牽扯的相關類別多到程式碼印出來可以環繞地球三圈,以下是用 NDepend 分析出來所有與 HttpContext 類別相關的所有類別,紅色是 HttpContext 類別的程式碼,藍色部分就是所有相關的:
由於我們不瞭解 IgnoreRoute 與 MapRoute 的實做細節,所以我們不可能知道到底需要 Mock 多少物件,我們也很難寫出一個「可信任」的單元測試程式。因此,我們需要一個完整的 Stub 物件,幫助我們讓 IgnoreRoute 與 MapRoute 都能正確無誤的執行,並回傳如預期的結果!
由於 ASP.NET MVC 擁有非常優秀的可測試性,除此之外,也已經有不少人貢獻了許多方便單元測試的 Stub 類別與輔助方法,所以我們不用重新發明輪子,直接使用 MvcContrib 專案的 Unit Testing Library ( TestHelper ) 即可擁有許多非常好用的 Stub 類別。
我們先下載 MvcContrib 的組件回來:
解壓縮之後找出以下三個組件,並加入到你的測試專案中:
接著我們新增一個 MvcApplicationTest.cs 測試類別:
然後再類別裡寫一個測試方法:
如上程式碼依序號解釋如下:
- 我們先取得一個空的 RouteCollection 以便傳入 MvcApplication.RegisterRoutes 執行
- 利用 MvcContrib 專案的 Unit Testing Library 內建的 FakeHttpContext 類別得到一個 Stub 物件
註: 這時會傳入模擬的 URL 與 HTTP Method ( 如 GET 或 POST ) - 執行 MvcApplication.RegisterRoutes 方法 ( 這時我們會得到被設定過 Routing 資料的 route 物件 )
- 執行 routes.GetRouteData 方法並傳入 context (Stub 物件) 以運算出接近真實的 RouteData 物件
- 最後驗證 RouteValue 是否如預期的正確
執行測試,得到正確的結果:
如果我們沒有 FakeHttpContext 類別幫我們產生 Stub 物件,我想幾乎是沒辦法測試 Routing 機制的,這個 FakeHttpContext 類別還沒那麼單純,若沒有另外再模擬出 FakeHttpRequest、FakeHttpRequest、FakeHttpResponse、FakeHttpSessionState 這些 Stub 的話,事實上還是不夠完整的,我們可以利用 Reflector 工具查看相關程式與命名空間:
由以上描述應該可以得知 Mock 與 Stub 的主要差異了吧,我再次大膽的將 Mock 與 Stub 二分如下:
- Mock 物件適用於較為簡易的測試情境
- Stub 物件適用於模擬較為複雜的情境資訊 (Context Information)
這樣二分法其實並不好,因為 Stub 物件也可以用來模擬簡易的測試情境,Mock 也可以用來模擬複雜的情境,所以這份拿捏並不容易,不過你也可以參考以下意見進行判斷:
- 建立 Mock 物件的寫法比較簡單、直覺,不用預先建立類別,撰寫的速度非常快,節省時間
- 建立 Stub 物件需要花較長的時間撰寫相關程式,需完整瞭解被測試程式的運作細節才能寫出適合的 Stub 類別
另一個從程式的外觀上的差異是:
- Mock 不用先建立物件即可透過 DynamicProxy 的機制動態生成類別與自動建立物件實體
- Stub 需預先建立可模擬真實狀況的類別,並且需要在測試程式中明確建立物件實體
---
單元測試首重觀念正確,否則測試程式會越寫越灰心的,當然,我也不敢保證我說的一定正確,但至少我寫的這些觀念都是我深思熟慮過後的產物,歡迎各位留言探討問題或思路打結的地方,我將盡可能回答各位的問題,如有謬誤之處也請放心指教,謝謝。
程式碼下載
相關連結