🎯 什麼情境該想到我
當你「在 C/C++ 裡想把某個類別成員函式在測試時整個換掉,而它的宣告和定義是分開的」的時候。
⚙️ 怎麼用(步驟)
原理(p.266):「有些語言允許你在一個地方宣告型別然後在另一個地方定義它。這一點體現得最明顯的就是在 C/C++ 中。在 C/C++ 中我們可以在一處地方宣告一個函式/方法,然後在另一處地方(通常是實作檔案中)定義它。這一能力可以用來幫助我們解依賴。」
做法(p.267):在測試檔案中包含目標類別的標頭檔,然後在測試檔案裡給目標方法提供另一個定義。「只需在測試中直接定義有關方法,我們便可以提供只用於測試的方法定義。我們可以給那些我們在測試時並不關心的方法定義一個空的方法體,也可以定義可用於所有測試的感知方法。」
步驟(p.268,書中標明「在 C++ 中使用定義補全技術的步驟」)
- 找出你想要對其成員函式實施定義替換的類別。
- 確認該類別的成員函式定義是在源檔案而非標頭檔中。
- 將該標頭檔包含到待測試類別的測試源檔案中。
- 確保該類別的源檔案並不參與建置。
- 建置,找出沒替換定義的成員函式。
- 往測試源檔案中添加相應的成員函式定義,直到建置成功。
★★ 在 Go 裡:完全不存在(它是 C/C++ 的連結期技巧)
/* ★ C 的做法:★★ header 宣告、測試檔提供另一份定義 */
/* ★ 因為 C 的「宣告」與「定義」是分開的,★ 連結時才綁定 */★★ Go 沒有這個可能,原因是結構性的:
① ★ Go 沒有 header/實作檔的分離 —— ★★ 宣告與定義在同一個地方
② ★ Go 的 package 是編譯單元 ⇒ ★★ 同一個 package 內不能有兩份同名定義
③ ★ 連結是 go build 的內部細節,★★ 使用者無法介入符號解析
★★ 所以這一頁在 Go 的價值純粹是「知道它為什麼不適用」——
★ 而它指向一個更有用的問題:★★ Go 裡「不改程式碼就替換實作」有哪些手段?
★★ Go 裡最接近的三種「不改呼叫端程式碼」的替換
// ★★ ① build tag(★ 檔案層級的替換)—— 見 [[連接替換]]
//go:build !test
package clock
func Now() time.Time { return time.Now() }
//go:build test
package clock
func Now() time.Time { return fixedTime }
// ⚠️ ★★ 但 Go 不鼓勵用自訂 build tag 做測試替換:
// ★ 它讓「測試跑的碼」和「生產跑的碼」是不同的檔案
// → ★★ 而那正是 Feathers 警告過的:★ 你測的不是你部署的
// ★ ② go.mod 的 replace(★ 模組層級的替換)
// go mod edit -replace=example.com/lib=./local-fake
// → ★ 用於「換掉一整個依賴」,★★ 而不是單元測試的 seam
// ★★ ③ _test.go 檔案裡的 export_test.go 慣例(★ 這才是 Go 的做法)
// ★ package 內的測試可以存取未匯出的東西;
// ★★ export_test.go 可以「為測試匯出」一個 seam:
// var TimeNow = timeNow // ★ 只在測試 build 中存在★★ ③ 是 Go 標準庫自己用的做法,而它的好處是:
★ 生產程式碼完全不變,★★ 而 seam 只存在於測試 build 裡。
★ 這一頁真正的教訓(跨語言都成立)
★★ Feathers 分類這些手法時有一條隱含的排序:
★ 越不需要改生產程式碼的手法,越安全(★ 因為改動就是風險)
→ ★★ 但也越依賴語言/工具的特性
→ ★ Go 選了「明確、無魔法」的一端 ⇒ ★★ 你必須改程式碼來建立 seam
→ ★ 而那個改動本身要靠 [[介面提取]] 這類「編譯器會抓錯」的手法來保證安全
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
書中對這條技術的保留意見是全章最重的,原文(p.267–268):
-
必須拆出獨立的測試執行檔:「在 C/C++ 中使用定義補全技術,這就意味著我們得為使用了定義補全的測試建立單獨的可執行檔了。因為如果不這麼做的話,替換的定義就會跟原有的定義在連接期產生衝突。」
-
同一個方法會有兩組定義:「一組位於測試源檔案中,另一組位於產品程式碼源檔案中。這可能會給程式碼維護帶來很大負擔。」
-
可能害你 debug 到錯的東西:「如果你的除錯環境沒有設置妥當的話,甚至可能會使除錯器載入了錯誤的除錯資料。」
-
作者明講不推薦:「因此,並不推薦使用該技術,除非你遇上了最糟糕的依賴情況。而且即使如此,我也建議你只把該技術用在解開初始依賴上。一旦解除了初始依賴,你應該就能快速地將類別置入測試之下,這時重複的定義便可以刪除了。」(p.267–268)
-
語言限定:這條技術依賴「宣告與定義分離」的語言能力,書中的說明與步驟都是針對 C/C++ 的。
-
整章共通的誠實取捨:「這些技術並不能立竿見影地讓設計變得更好。」(p.258)它們是要在沒有測試的情況下使用的,目的是「將測試安置到位」。
-
★★ Go 完全沒有這個可能(無 header/實作分離、package 是編譯單元、無法介入符號解析)。
-
★★ 不要用自訂 build tag 做單元測試替換 —— 它讓「測試跑的碼」和「生產跑的碼」是不同檔案,而那正是 Feathers 警告的「你測的不是你部署的」。
-
★ Go 的做法是
export_test.go:為測試匯出一個 seam,生產程式碼完全不變,而 seam 只存在於測試 build 裡。這是標準庫自己的做法。 -
★
go.mod的replace是模組層級的替換,用於換掉整個依賴,不是單元測試的 seam。 -
★ 越不需要改生產程式碼的手法越安全,但也越依賴語言特性。 Go 選了「明確、無魔法」的一端。