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