The Will Will Web

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

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

過年期間在家聽了 The Art of Unit Testing with Roy Osherove 節目,感覺獲益良多,因此將節目的對話內容做了個簡單的整理,由於是 Scott HanselmanRoy Osherove 的現場對話,內容感覺有點雜亂,我不確定可否將重點整理的夠清楚,但有些 Roy Osherove 分享的觀念是我覺得是很棒的,也解開我這段時間懵懵懂懂的 TDD 觀念,大家可以看完若覺得有問題可以留言討論看看。

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

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

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

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

  • 你是否能夠持續的維護你的測試程式?
  • 如何有效的降低維護測試程式的成本?
  • 註: 透過一些 Testable Design Pattern 可以有效提升可維護性。例如: Repository Pattern, Service Pattern, …。好的軟體架構也能增強可維護性,例如: High Cohesion, Low Coupling, SRP, …

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 來降低個別 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 的建議來看,這個 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,但經常不知道要測到哪種程度才算夠,我的裡解是:

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

相關連結