﻿# 與 Roy Osherove 探討單元測試的藝術 (心得筆記)

過年期間在家聽了 [The Art of Unit Testing with Roy Osherove](http://hanselminutes.com/default.aspx?showID=187) 節目，感覺獲益良多，因此將節目的對話內容做了個簡單的整理，由於是 [Scott Hanselman](http://www.hanselman.com/blog/) 與 [Roy Osherove](http://weblogs.asp.net/rosherove/) 的現場對話，內容感覺有點雜亂，我不確定可否將重點整理的夠清楚，但有些 [Roy Osherove](http://weblogs.asp.net/rosherove/) 分享的觀念是我覺得是很棒的，也解開我這段時間懵懵懂懂的 TDD 觀念，大家可以看完若覺得有問題可以留言討論看看。

要成功導入**單元測試**有三個主要的基本要素：

**1\. 信任你的測試結果 ( Trustworthiness )**

-   你是否能信任你的測試結果？
-   如果你不斷的對測試結果失去信心，那麼你也不會繼續堅持撰寫單元測試
-   有許多人搞不清楚「單元測試」與「整合測試」的差別，以致於感覺自己寫的單元測試過於薄弱而不相信測試的結果。（註: 該文章後面會提到**單元測試**與**整合測試**的差別）
-   如果你因為某些原因導致測試失敗，直接去改 Code 或直接去改 Test Code 都不是好事，你的首要目的是要能找出測試失敗發生的主因，而非只是看錯誤這件事，這樣你才能信任你的測試程式。

**2\. 測試程式的可維護性 ( Maintainability )**

-   你是否能夠持續的維護你的測試程式？
-   如何有效的降低維護測試程式的成本？
-   註: 透過一些 Testable Design Pattern 可以有效提升可維護性。例如: Repository Pattern, Service Pattern, …。好的軟體架構也能增強可維護性，例如: [High Cohesion](http://en.wikipedia.org/wiki/GRASP_%28Object_Oriented_Design%29#High_Cohesion), [Low Coupling](http://en.wikipedia.org/wiki/GRASP_%28Object_Oriented_Design%29#Low_Coupling), [SRP](http://en.wikipedia.org/wiki/Single_responsibility_principle), …

**3\. 測試程式的可讀性 ( Readability )**

-   你的測試程式的命名是否易於理解？
-   當你測試失敗時是否能從測試失敗的測試方法(TestMethod)明確看出實際失敗的原因？
-   當讀取測試數據的人看不懂你的測試，人們就不會執行這些測試、也不會去維護這些測試，久而久之就會越來越惡化。

\----------

**何謂「單元測試」？「單元測試」的注意事項！**

-   測試的最小單位，必須是可信任的、可重複執行的。  
    例如: 測試某一個類別(Class)的某一個方法(Method)
-   必須與「整合測試」做非常清楚的切割，兩者個概念完全不同
-   單元測試程式不應該接觸到任何與任何 **外部資源** (External Resources) 或 **靜態物件** (Statics)。例如: File I/O, 資料庫操作, 網路連線, … 等等，且由於靜態方法會牽扯到狀態，所以不建議在單元測試中存取任何靜態物件。
-   DAL (Data Access Layer) 的程式不建議撰寫「單元測試」

-   因為 DAL 的程式會與資料庫直接產生關聯，而資料庫中可能還會有 Trigger, Stored Procedure, 表格關聯, … 等，這些東西都會打破「單元測試」的原則，所以建議放入「整合測試」來進行。

-   在「單元測試」類別裡，通常不會用到 Setup Attribute 與 Teardown Attribute，如果為了要簡化各 TestMethod 中許多重複的程式碼，建議使用 [Factory method Pattern](http://en.wikipedia.org/wiki/Factory_method_pattern) 來降低個別 TestMethod 之間的耦合關係。
-   開發人員在撰寫程式時，務必要讓所有測試結果都亮起**綠燈**，沒有藉口。

**何謂「整合測試」？與「整合測試」的注意事項！**

-   針對軟體專案的一部份或全部進行測試，可以跨越不同的類別與方法，並可直接存取的外部資源。例如: File I/O, 資料庫操作, 網路連線, … 等等。
-   通常做「整合測試」都會需要先設置(Configure)測試所需的環境，測試完畢後通常要清除測試所產生的殘留資料，以利下次測試或避免影響其他整合測試的結果。

-   可套用 Setup Attribute
-   可套用 Teardown Attribute ( 或 Cleanup Attribute )
-   註: 通常你看到測試專案中有用到 Teardown Method (即套用 Teardown 屬性的方法) 通常就意味著這是一個「整合測試」的測試方法。

\----------

**什麼是 Fake 物件？**

-   基本上我們為了要測試，所以會準備一些**假的**資料，由於「單元測試」不會接觸到外部資源，所以會需要透過一些假的物件模擬輸入的資料，所以當我們提到 faking something 時，意思就是提供一個假的、模擬的資料而已。
-   Fake 物件包含了兩種定義：

-   Mock 物件
-   Stub 物件

**什麼是 Mock 物件？**

-   Mock 物件主要的目的是為了用來做驗證(Assert)測試的結果是通過還是失敗
-   所以 Mock 物件也是 Fake 物件的一種，用來假設該物件最後的執行結果為何，如果執行的結果與我們定義的 Mock 物件相符合，那就代表測試成功
-   通常一個單元測試的 TestMethod 中只會有一個 Mock 物件

**什麼是 Stub 物件？**

-   由於執行測試時可能會傳入許多參數或資料，為了能讓測試程式能夠執行，我們必須準備一些資料讓「單元測試」程式能夠順利執行，所以所有與驗證結果無關的 Fake 物件都稱為 **Stub 物件**
-   通常一個單元測試的 TestMethod 中可能會有多個 Stub 物件，目的只是為了讓測試能夠順利進行

\----------

**「整合測試」與「單元測試」的差異，以一個簡單的例子解釋**

如果你有一個 Storage 類別，該類別有個 Method 要被測試，該 Method 會呼叫 Service Layer 中的一個 Logger 物件，該物件可能是一個 Web Service 或直接存取資料庫。

而我們在撰寫**單元測試**時僅需 **驗證** (**Assert**) 是否呼叫 Logger 物件的方法即可，並不需要讓 Logger 物件真正執行寫入 Log 或執行 Web Service 等動作，所以在 TestMethod 中你可能需要建立個 Fake Logger 物件(Mock Object)，並且讓 Fake Logger 物件能透過 Mock Framework 或 Isolation Framework 之中獨立出來，並直接驗證其執行結果。若更進一步你可以在「單元測試」中撰寫一些輸入資料驗證的狀況，例如等 Log Message 大於 512 bytes 時引發例外狀況…等等。

如果是在做**整合測試**時，就必須要驗證「**Log 檔案是否被建立或寫入**」或「**資料是否正確寫入資料庫**」等等。

「單元測試」確保程式執行的結果如你預期的執行，「整合測試」確保程式執行的行為與物件之間互動的情況，這兩者之間的測試程式也許類似，但需要被驗證(Assert)的東西可能不太一樣，而就是「整合測試」與「單元測試」最主要的差異。

\----------

**如何聞出「單元測試程式」的壞味道(Code Smell)**

-   每個 TestMethod 之間不能有任何關聯

-   有些人為了在測試專案避免重複的程式碼，會讓其中一個 TestMethod 呼叫另一個 TestMethod，但這反而可能會影響測試程式之間的結果

-   每個 TestMethod 之間不能有任何執行順序的要求。

-   例如: 要先執行 TestMethodA 再執行 TestMethodB，這就是一個臭掉的 Code

-   在一個 TestMethod 不要同時間驗證(Assert)**兩件事**以上

-   一個 TestMethod 中如果有兩個以上的 Assert 命令，通常 90% 的情況都是有問題的，但如果這兩個以上的 Assert 命令是為了驗證同一件事，那就可以接受。
-   重點在於你所定義的 TestMethod 是不是一個完整的「邏輯測試單位」，如果你測試的是「一件事」但有多個可以驗證的目標，那就可以擁有多的 Assert 驗證。
-   例如在 ASP.NET MVC 2 RC 的 Visual Studio 2008 專案範本中內建的單元測試範例就有一個不好的例子（如下程式碼），從「測試方法的命名」看來是要驗證回傳結果是不是一個 View，但是他在同一個 TestMethod 中同時也驗證了 ViewData 的值，依照 [Roy Osherove](http://weblogs.asp.net/rosherove/) 的建議來看，這個 ChangePassword\_Get\_ReturnsView() 應該要拆開成兩個 TestMethod 才對：

```
[TestMethod]public void ChangePassword_Get_ReturnsView(){    // Arrange    AccountController controller = GetAccountController();    // Act    ActionResult result = controller.ChangePassword();    // Assert    Assert.IsInstanceOfType(result, typeof(ViewResult));    Assert.AreEqual(10, ((ViewResult)result).ViewData["PasswordLength"]);}
```

\----------

**單元測試中的 TestMethod 的命名規則**

-   方法名稱會切割成三段，每段以底線 ( \_ ) 分隔
-   第 1 段：被測試的 Method 名稱
-   第 2 段：測試的情境(Scenario)
-   第 3 段：預期測試的結果
-   並不以「單字」作為分段的依據，而是以這三段的分別來分隔
-   例如：

-   第 1 段：我們要測試 ChangePassword 方法
-   第 2 段：情境是透過 HTTP GET 取得頁面時
-   第 3 段：預期會回傳一個 ViewResult

-   這時該測試方法的命名就可以為：

-   ChangePassword\_Get\_ReturnViewResult

\----------

**對測試專案的組織方法**

-   假設你原本專案名稱為：ProjectX
-   單元測試專案可命名為：ProjectX.Tests
-   整合測試專案可命名為：ProjectX.IntegratedTests
-   剛開始導入單元測試建議一個專案對應一個單元測試專案，先不要多個專案共用一個單元測試專案

\----------

**單元測試到底要寫多少才夠？**

還記得「**單元測試**有三個成功因素」的第 1 項嗎？**信任你的測試結果 ( Trustworthiness ) ！**

單元測試要寫到能讓自己有信心的地步為止，只要你寫到感覺放心了，那測試程式就算寫夠了，而寫到多少、多細才算真的放心？那就看個人了，也因為沒有標準，所以單元測試才被稱為一種「藝術」。

有些人會花許多時間測試使用者可能輸入的值，尤其是 [Monkey Test](http://en.wikipedia.org/wiki/Monkey_test)，但經常不知道要測到哪種程度才算夠，我的裡解是：

-   先測試合理的輸入值，確認功能與架構
-   再測試無理的輸入值，確認超出範圍的輸入值可否被正確處理

**相關連結**

-   [Hanselminutes - Show# 169 - The Art of Unit Testing with Roy Osherove](http://hanselminutes.com/default.aspx?showID=187)

-   [The Art of Unit Testing: With Examples in .Net](http://www.amazon.com/gp/product/1933988274?ie=UTF8&tag=diabeticbooks&linkCode=as2&camp=1789&creative=390957&creativeASIN=1933988274)
-   [Art of Unit Testing - Official Book Wiki](http://www.artofunittesting.com/)
-   [TypeMock](http://www.typemock.com/)
-   [Roy's Blog](http://weblogs.asp.net/rosherove/)
-   [Scott Hanselman's blog](http://www.hanselman.com/blog/)

-   [GRASP (Object Oriented Design) - Wikipedia, the free encyclopedia](http://en.wikipedia.org/wiki/GRASP_%28Object_Oriented_Design%29)
-   [Single responsibility principle - Wikipedia, the free encyclopedia](http://en.wikipedia.org/wiki/Single_responsibility_principle)
-   [Factory method pattern - Wikipedia, the free encyclopedia](http://en.wikipedia.org/wiki/Factory_method_pattern)
