單元測試

2014 年 5 月 5 日

在軟體開發中,單元測試經常被提及,而且在我撰寫程式碼的整個過程中,這是一個我熟悉的術語。然而,就像大多數軟體開發術語一樣,它的定義非常模糊,而且我發現,當人們認為它的定義比實際更嚴謹時,經常會產生混淆。[1]

雖然我之前已經做了很多單元測試,但我明確接觸到它是在我開始與 Kent Beck 合作並使用 Xunit 單元測試工具系列的時候。(事實上,我有時認為「xunit 測試」可能是這種測試風格的一個好術語。)單元測試也成為 極限程式設計(XP)的一項標誌性活動,並迅速導致 測試驅動開發

從一開始,對於 XP 使用單元測試的定義就存在疑慮。我清楚記得在一個 usenet 討論群組中的一場討論,當時我們這些 XP 使用者被一位測試專家斥責濫用「單元測試」一詞。我們請他給出他的定義,他回答說:「在我的培訓課程的第一個早上,我涵蓋了 24 種不同的單元測試定義。」

儘管有差異,但有一些共同的元素。首先,有一個概念,即單元測試是低階的,專注於軟體系統的一小部分。其次,單元測試通常是由程式設計師自己使用他們的常規工具編寫的,唯一的區別是使用某種單元測試架構[2]。第三,預計單元測試將顯著快於其他類型的測試。

因此,有一些共同的元素,但也有差異。一個區別是人們認為什麼是單元。物件導向設計傾向於將類別視為單元,程序或函數式方法可能會將單一函數視為單元。但實際上這是一個情境性的事物,團隊決定什麼對於他們理解系統及其測試而言有意義。雖然我從單元是類別的概念開始,但我經常將一堆密切相關的類別視為一個單元。我很少將類別中的一組方法視為一個單元。然而,您如何定義它並不重要。

獨自或群居?

一個更重要的區別在於你測試的單元應該是群居的還是獨自的 [3]。想像你正在測試一個訂單類別的價格方法。價格方法需要在產品和客戶類別上呼叫一些函式。如果你喜歡你的單元測試是獨自的,你不想在此處使用真實的產品或客戶類別,因為客戶類別中的錯誤會導致訂單類別的測試失敗。相反,你可以為合作者使用 測試替身

但並非所有單元測試人員都使用獨自單元測試。事實上,當 xunit 測試在 90 年代開始時,我們沒有嘗試獨自進行,除非與合作者溝通很尷尬(例如遠端信用卡驗證系統)。即使它導致鄰近測試失敗,我們也發現追蹤實際錯誤並不困難。因此,我們認為允許我們的測試群居並不會導致實際問題。

事實上,使用群居單元測試是我們因使用「單元測試」一詞而受到批評的原因之一。我認為「單元測試」一詞是恰當的,因為這些測試是單一單元行為的測試。我們撰寫測試,假設除該單元之外的所有內容都正確運作。

隨著 xunit 測試在 2000 年代變得越來越流行,獨自測試的概念又回來了,至少對一些人來說是這樣。我們看到模擬物件和支援模擬的架構興起。發展了兩種 xunit 測試流派,我稱之為經典和模擬風格。這兩種風格之間的其中一個區別在於,模擬主義者堅持獨自單元測試,而經典主義者則偏好群居測試。今天我認識並尊重這兩種風格的 xunit 測試人員(我個人一直堅持經典風格)。

即使像我這樣的經典測試人員,在遇到尷尬的合作時也會使用測試替身。它們對於在與遠端服務通話時消除 非決定論非常有價值。事實上,一些經典主義 xunit 測試人員也主張,與外部資源(例如資料庫或檔案系統)的任何合作都應該使用替身。這部分是出於非決定論風險,部分是出於速度。雖然我認為這是一個有用的準則,但我不會將對外部資源使用替身視為絕對規則。如果你與資源的通話穩定且足夠快,那麼在你的單元測試中執行它沒有任何理由。

速度

單元測試的共用特性 — 範圍小、由程式設計師自行執行、執行速度快 — 表示在編寫程式時可以非常頻繁地執行。這的確是 SelfTestingCode 的主要特性之一。在這種情況下,程式設計師會在對程式碼進行任何變更後執行單元測試。我可能會在幾分鐘內執行單元測試數次,只要我有值得編譯的程式碼時。我會這麼做,是因為如果我不小心弄壞了什麼,我希望馬上就知道。如果我透過最近的變更引入了缺陷,我就能更容易找出錯誤,因為我不用找很遠。

當你如此頻繁地執行單元測試時,你可能不會執行所有單元測試。通常你只需要執行那些針對你目前正在處理的程式碼部分進行運作的測試。一如往常,你必須在測試深度與執行測試套件所需時間之間取得平衡。我會將此套件稱為編譯套件,因為這是每當我想編譯時執行的套件,即使是在像 Ruby 這樣的直譯語言中。

如果你正在使用持續整合,你應該執行測試套件作為其中的一部分。這個套件(我稱之為提交套件)通常會包含所有單元測試。它也可能包含一些 BroadStackTests。作為一名程式設計師,你應該每天執行這個提交套件數次,當然是在對版本控制進行任何共用提交之前,但在你有機會的任何其他時間也可以執行,例如休息或必須參加會議時。提交套件執行得越快,你就能越常執行它。 [4]

不同的人對單元測試及其測試套件的速度有不同的標準。 David Heinemeier Hansson 對於執行數秒的編譯套件和執行數分鐘的提交套件感到滿意。 Gary Bernhardt 認為這慢得令人難以忍受,堅持編譯套件大約需要 300 毫秒,而 Dan Bodart 不希望他的提交套件超過十秒

我不認為這裡有絕對的答案。就我個人而言,我沒有注意到小於一秒或幾秒的編譯套件之間的差異。我喜歡 Kent Beck 的經驗法則,即提交套件的執行時間不應超過十分鐘。但重點在於你的測試套件應該執行得夠快,才不會讓你卻步,無法頻繁執行它們。而頻繁執行是指在它們偵測到錯誤時,你只需要檢視極少量的程式碼就能快速找出錯誤。

備註

1: 我在 IntegrationTest 的條目中寫了一點關於名稱歷史根源的內容

2: 我說「這些天」是因為這肯定是因為 XP 而改變的事情。在世紀之交的辯論中,XPer 因為這點而受到嚴厲批評,因為普遍的觀點是程式設計師絕不應該測試自己的程式碼。有些商店有專門的單元測試人員,其全部工作就是為開發人員稍早寫的程式碼撰寫單元測試。原因包括:人們在測試自己的程式碼時觀念盲點、程式設計師不是好的測試人員,以及開發人員和測試人員之間有對抗關係是好的。XPer 的觀點是程式設計師可以學會成為有效的測試人員,至少在單元層級,而且如果你讓一個獨立的群組參與,測試提供的回饋迴路將會慢得令人絕望。Xunit 在這裡扮演了重要的角色,它特別設計成將程式設計師撰寫測試的摩擦降到最低。

3: Jay Fields 提出了「孤獨」和「社交」這些術語

4: 如果你有有用的測試,但執行時間比你希望提交套件執行的時間長,那麼你應該建立一個 DeploymentPipeline,並將較慢的測試放在管線的後續階段。

修訂

於 2014 年 10 月 24 日更新,加入 Field 的社交/孤獨詞彙。

於 2017 年 3 月 9 日更新,將孤獨/社交術語作為描述區別的主要方式,並移除「協作者隔離」一詞的使用(因為會與將測試固定裝置變更彼此隔離混淆)。