探討單元測試和整合測試的涵蓋範圍

探討單元測試和整合測試的涵蓋範圍

本篇文章紀錄自己導入 測試驅動開發(Test Driven Design) 過程中,曾經沒辦法分辨自己所寫的測試案例到底是“單元測試”還是“整合測試”,與同儕討論後發現其他人也有相同的困擾,於是看了幾本書與文章才釐清自己的問題所在。為方便與其他人進行交流討論,故將自己理解的資訊整理下來並做個總結。

單元測試的涵蓋範圍很模糊?

單元測試(Unit Test)是軟體開發中很重要的環節,替 TDD 提供重構的保護網,也是軟體測試(Software Testing)中測試金字塔(Test Pyramid)的最低測試層級。

但是,一個「單元測試」所涵蓋的範圍到底有哪些,卻讓國外網友議論紛!

大家在初學單元測試一定會看到的定義如下:

以程式碼的最小單位來進行正確性檢驗的測試工作,最小單位包括「類別與方法」。

若按此定義來寫測試案例,一個單元測試只能包含一個類別。且受測類別的依賴都必須透過測試替身或 Mock 技術進行隔離,才能確保測試的目標是最小且不可分割的邏輯。

但是隨著 Mock 的詬病被發掘(參考:Mock 不是測試的銀彈),為避免 Mock 使測試案例變成開發人員的快樂表(測試通過,正式環境卻出現錯誤),開始有人提倡使用 Spy 來替代 Mock,以及依賴若是自己的開發團隊所寫,而非第三方函式庫,則可直接使用依賴。

這時,一個單元測試會執行的範圍已經從 一個類別 變成 一個類別加上該類別的依賴。換句話說,一個單元測試除了受測程式外,也會執行到其他類別的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 找到答案,原來單元測試的涵蓋範圍有兩派!

孤立型(Solitary)or 社交型(Sociable)

圖一:Martin Fowler:Unit Test

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 在 Twitter 的發言

原文連結:
Uncle Bob 在 Twitter 的發言

最後,Uncle Bob 在後續留言還有補充 TDD 單元測試的測試案例應該寫在哪個層級:

TDD 單元測試的測試案例應該寫在哪個層級

總結

TDD/BDD 與軟體品質(QA)的單元測試很容易混淆,但兩者的目的與涵蓋範圍並不相同。

若對兩種單元測試的本質不夠了解,就容易在寫測試案例的時候陷入進退兩難的窘境,因此釐清自己正在使用哪一種單元測試相當重要!若是帶領一個開發團隊,一定要在動手開發之前讓團隊要有一個統一的語言和定義。否則,做出來的結果可能相當不一樣呢!

推薦閱讀:

留言

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×