本篇文章紀錄自己導入 測試驅動開發(Test Driven Design) 過程中,曾經沒辦法分辨自己所寫的測試案例到底是“單元測試”還是“整合測試”,與同儕討論後發現其他人也有相同的困擾,於是看了幾本書與文章才釐清自己的問題所在。為方便與其他人進行交流討論,故將自己理解的資訊整理下來並做個總結。
單元測試的涵蓋範圍很模糊?
單元測試(Unit Test)是軟體開發中很重要的環節,替 TDD 提供重構的保護網,也是軟體測試(Software Testing)中測試金字塔(Test Pyramid)的最低測試層級。
但是,一個「單元測試」所涵蓋的範圍到底有哪些,卻讓國外網友議論紛!
大家在初學單元測試一定會看到的定義如下:
以程式碼的最小單位來進行正確性檢驗的測試工作,最小單位包括「類別與方法」。
若按此定義來寫測試案例,一個單元測試只能包含一個類別。且受測類別的依賴都必須透過測試替身或 Mock 技術進行隔離,才能確保測試的目標是最小且不可分割的邏輯。
但是隨著 Mock 的詬病被發掘(參考:Mock 不是測試的銀彈),為避免 Mock 使測試案例變成開發人員的快樂表(測試通過,正式環境卻出現錯誤),開始有人提倡使用 Spy 來替代 Mock,以及依賴若是自己的開發團隊所寫,而非第三方函式庫,則可直接使用依賴。
這時,一個單元測試會執行的範圍已經從 一個類別 變成 一個類別加上該類別的依賴。換句話說,一個單元測試除了受測程式外,也會執行到其他類別的程式碼:
1 | describe('AddGroupToRange', function () { |
如上範例所見,此測試案例已包含多個類別的邏輯。
但是,按照一開始所學的「單元測試定義」,我開始懷疑自己寫的測試案例到底算不算單元測試呢?
原來單元測試涵蓋範圍有兩派?
為解決疑慮,我到開始找人討論、爬文試圖找出單元測試的涵蓋範圍。最後在 Martin Fowler 的文章 UnitTest 找到答案,原來單元測試的涵蓋範圍有兩派!
孤立型(Solitary)or 社交型(Sociable)
Martin Fowler1 認為,在撰寫單元測試時,搞清楚自己的測試案例屬於 孤立型(Solitiary) 還是 社交型(Sociable) 很重要!
如果你喜歡使用 孤立型的單元測試,那麼 受測物件將不會使用真實的依賴類別。因為依賴類別發生錯誤,也會造成單元測試無法通過!為了確保受測程式不被影響,孤立型單元測試 會利用測試替身(Test Doubles)模擬並隔離依賴(如圖一右方)。
如果你喜歡 社交型的單元測試,則 受測物件會直接使用真實的依賴類別,讓測試案例真實地執行一個完整的行為。
Martin Folwer 也提及,社交型單元測試的作法可能會因「單元測試的定義」而被抨擊。但他覺得這並不是什麼問題,他認為:
because these tests are tests of the behavior of a single unit.
單元測試是對一個行為的測試。
我們在測試一個行為時,也會「假設」受測行為以外的功能都是正常的。這種「假設」本質上與 孤立型的單元測試 是一樣的!
(題外話:Martin Fowler 在文章中表明自己偏好社交型的單元測試)
TDD/BDD 是社交型單元測試嗎?
在《修改軟件的藝術》第 10 章測試先行,作者提及 TDD 的單元測試與狹義的單元測試不同,TDD 是以 一個行為 作為一個單元:
一個獨立、可驗證的行為。這個行為會對系統產生可觀察的影響,且不和系統的其他行為耦合。
這個單元測試的定義意味著:每個可觀察到的行為都應該要有一個相對應的測試。
另外在《Growing Object-Oriented Software, Guided by Tests》第五章節也指出,應該針對行為進行單元測試,而非針對方法。
這下真相大白了!如果你是 BDD 或 TDD 的實踐者,那麼你的單元測試就可能是跨多個類別的 社交型單元測試,因為測試的對象是 一個行為,而非一個類別。
TDD 並不能取代品質保證
TDD 所編寫的測試,目的是為 系統重構(Refactoring) 提供支持。本質上與 QA 團隊做的軟體品質測試並不相同,因此狹義、細粒度 以品質保證為目標的單元測試 仍然有其存在的價值。
兩種單元測試的差異:
項目 | QA 的單元測試 | TDD/BDD 的單元測試 |
---|---|---|
目的 | 檢驗軟體基本組成單位的正確性 | 建立回歸測試,讓系統支持重構 |
單元的定義 | 最小且不可分割的邏輯 | 獨立、可驗證的行為 |
測試粒度 | 一個類別或一個函式 | 一個類別或一群依賴關係緊密的類別 |
社交型單元測試也算整合測試嗎?
曾經我也有這個疑問,以為自己寫的單元測試其實是整合測試吧?!
會有這種錯覺,也是來自下面這條整合測試的定義:
- 對不同模組之間的交互作用進行測試
但是測試案例成為整合測試的關鍵點是:測試案例是否包含與外部環境交互的邏輯,如時間、Session、Cookie、資料庫,硬體,網路等等不受程式控制的因素。
簡單來說,若測試案例無與外部環境交互的邏輯,則可以將測試案例視為單元測試:
反之,若測試案例中包含與外部環境交互的邏輯,那麼這個測試案例就是一個整合測試:
[補充]
Uncle Bob 對 TDD 單元測試的看法:
單元測試的定義有兩個版本,在國外好像越來越被接受了,但是國內卻還不是很明確。
2017 年,Uncle Bob 在 Twitter 有對網友說明 TDD 單元測試的對象是一個“行為”,而非一個“方法”:
最後,Uncle Bob 在後續留言還有補充 TDD 單元測試的測試案例應該寫在哪個層級:
總結
TDD/BDD 與軟體品質(QA)的單元測試很容易混淆,但兩者的目的與涵蓋範圍並不相同。
若對兩種單元測試的本質不夠了解,就容易在寫測試案例的時候陷入進退兩難的窘境,因此釐清自己正在使用哪一種單元測試相當重要!若是帶領一個開發團隊,一定要在動手開發之前讓團隊要有一個統一的語言和定義。否則,做出來的結果可能相當不一樣呢!
推薦閱讀:
- Victor:Defining Test Boundaries
- Martin Fowler:UnitTest
- 修改軟件的藝術
- 1.《重構:改善既有程式的設計》作者 ↩