TDD 反模式(TDD Anti-Patterns)
是什麼?
TDD Anti-Pattern 是那些表面上遵循 TDD 流程,但實際上違反 TDD 精神的測試寫法。這些壞味道會讓測試變成負擔而非安全網。
⚠️測試債務
壞測試比沒有測試更危險——它們給你虛假的安全感,同時每次重構都瘋狂報錯,逼你花大量時間維護測試而非改善產品。
常見反模式
The Liar(說謊者)
- 症狀:測試覆蓋率很高,但線上還是經常出 Bug;測試從來不會失敗,即使你故意改壞產品程式碼
- 原因:Assert 太寬鬆(如只檢查
not null)或根本沒有 Assert,測試永遠綠燈但什麼都沒驗證 - 修正:檢查每個測試的 Assert 是否驗證了具體的業務結果。把
Assert.NotNull(result)改成Assert.Equal(expectedValue, result.Property)
The Giant(巨人)
- 症狀:一個測試方法超過 50 行,失敗時完全不知道哪個斷言出問題
- 原因:違反「一個測試一個概念」原則,把多個場景塞進同一個測試
- 修正:拆成多個小測試,每個只驗證一個行為。命名用
Method_Scenario_Expected格式讓每個測試的目的一目瞭然
The Mockery(Mock 狂魔)
- 症狀:每個測試有 10+ 行 Mock Setup,重構時 Setup 全部要改;測試只驗證
Verify而不檢查業務結果 - 原因:Mock 了所有依賴包括不需要 Mock 的領域物件,測試只驗證 Mock 框架能正確運作
- 修正:只 Mock 外部邊界(HTTP、DB、第三方 API),領域物件和 Value Object 用真實實例。用 Fake(如 InMemoryRepository)取代過度的 Mock
Ice Cream Cone(冰淇淋甜筒)
- 症狀:CI 跑一次要 30+ 分鐘,測試經常隨機失敗(Flaky Test),開發者開始忽略測試結果
- 原因:Test Pyramid 倒過來——大量的 E2E 測試、很少 Unit Test。E2E 天生慢且不穩定
- 修正:不要一次全改。策略:新功能強制從 Unit Test 開始寫,舊功能在修 Bug 時補 Unit Test,讓金字塔慢慢長出來
The Inspector(窺探者)
- 症狀:重構內部結構(例如改變私有方法名稱或拆分方法)後,大量測試壞掉,即使外部行為完全沒變
- 原因:測試直接存取私有欄位(如
cart._items)或呼叫私有方法(如cart._calculateSubtotal()) - 修正:只透過公開介面驗證行為。如果你覺得「不測私有方法就沒辦法覆蓋到」,代表該方法應該被 Extract 成一個獨立的、可測試的類別
辨識與修正流程
辨識壞味道
測試常常無故失敗?跑很慢?改一行壞十個?這些都是警訊
分類反模式
對照常見反模式清單,判斷屬於哪一種壞味道
評估影響
這個反模式造成多大的維護成本?優先修最痛的
重構測試
套用對應的修正策略,把測試改成健康的版本
驗證改善
確認測試仍然能抓到真正的 bug,且不再因重構而壞掉
程式碼範例:Bad vs Good
The Mockery — C# (xUnit)
// BAD: Mock 了所有東西,測試毫無意義
[Fact]
public void PlaceOrder_Bad_MockEverything()
{
var mockRepo = new Mock<IOrderRepository>();
var mockValidator = new Mock<IOrderValidator>();
var mockPricer = new Mock<IPricingService>();
mockValidator.Setup(v => v.IsValid(It.IsAny<Order>())).Returns(true);
mockPricer.Setup(p => p.Calculate(It.IsAny<Order>())).Returns(100m);
var service = new OrderService(mockRepo.Object, mockValidator.Object, mockPricer.Object);
service.PlaceOrder(new Order());
// 只驗證「有沒有呼叫」,完全沒驗證業務邏輯
mockRepo.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);
}
// GOOD: 只 Mock 外部依賴,驗證真正的業務結果
[Fact]
public void PlaceOrder_Good_VerifyBehavior()
{
var fakeRepo = new InMemoryOrderRepository();
var validator = new OrderValidator(); // 用真的
var pricer = new PricingService(); // 用真的
var service = new OrderService(fakeRepo, validator, pricer);
service.PlaceOrder(new Order { Items = new[] { new Item("Book", 2) } });
var saved = fakeRepo.GetAll().Single();
Assert.Equal(2, saved.Items.Count());
Assert.True(saved.TotalPrice > 0);
}The Liar — TypeScript (Jest)
// BAD: 測試永遠通過,但沒有驗證任何東西
test('should process payment', () => {
const result = processPayment({ amount: 100, currency: 'USD' });
expect(result).toBeTruthy(); // 只要不是 null/undefined 就過
});
// GOOD: 明確驗證業務邏輯
test('should deduct amount and return success receipt', () => {
const result = processPayment({ amount: 100, currency: 'USD' });
expect(result.status).toBe('success');
expect(result.receipt.amount).toBe(100);
expect(result.receipt.currency).toBe('USD');
expect(result.receipt.timestamp).toBeDefined();
});The Inspector — Python (pytest)
# BAD: 測試私有方法,綁死內部實作
def test_bad_inspect_private():
cart = ShoppingCart()
cart.add_item("book", 30)
# 直接戳私有屬性
assert cart._items["book"] == 30
assert cart._calculate_subtotal() == 30
# GOOD: 透過公開介面驗證行為
def test_good_verify_public_behavior():
cart = ShoppingCart()
cart.add_item("book", 30)
cart.add_item("pen", 10)
assert cart.item_count() == 2
assert cart.total() == 40
assert "book" in cart.list_items()反模式關係圖
此圖展示五種反模式分別導致的後果:The Liar 和 The Mockery 給你虛假安全感(以為有測試保護但其實沒有);The Giant 和 The Inspector 產生脆弱測試(稍微重構就全部壞掉);Ice Cream Cone 讓測試執行緩慢(CI 變成團隊的瓶頸)。
快速自我檢查清單
用以下問題檢查你的測試是否有反模式:
- 故意改壞一行產品程式碼,有測試會失敗嗎? 如果沒有 → 可能是 The Liar
- 一個測試失敗時,你能在 10 秒內知道原因嗎? 如果不能 → 可能是 The Giant
- 重構不改行為,測試會壞嗎? 如果會 → 可能是 The Inspector 或 The Mockery
- CI 跑完測試要多久? 如果超過 10 分鐘 → 可能是 Ice Cream Cone
- 每個測試的 Mock Setup 超過 5 行嗎? 如果是 → 可能是 The Mockery
實戰補充
ℹ️資深開發者心得
在大型專案中,The Mockery 是最常見的反模式。團隊為了追求 100% 覆蓋率,把所有依賴都 Mock 掉,結果測試只驗證了「Mock 框架能正確運作」。正確做法是:只 Mock 你無法控制的外部邊界(HTTP、DB、第三方 API),領域邏輯用真實物件。
💡Ice Cream Cone 的解法
如果你的團隊已經陷入 Ice Cream Cone,不要試圖一次全改。策略是:新功能強制寫 Unit Test,舊功能在修 Bug 時補 Unit Test,讓金字塔慢慢長出來。
理解測驗
🤔 The Liar 反模式的核心問題是什麼?
🤔 當你重構內部實作後,大量測試壞掉,最可能是哪個反模式?
🤔 面對 Ice Cream Cone 問題,最務實的改善策略是?
重點整理
💡一句話記住
口訣:「壞測試比沒測試更毒——虛假安全感 + 維護地獄,雙重打擊」。
| 反模式 | 症狀 | 修正方向 | |--------|------|----------| | The Liar | 斷言太寬鬆,永遠綠燈 | 寫精確的斷言,驗證具體值 | | The Giant | 一個測試驗證太多東西 | 拆成多個小測試,一個測試一個概念 | | The Mockery | Mock 了所有依賴 | 只 Mock 外部邊界,領域邏輯用真實物件 | | Ice Cream Cone | E2E 多、Unit 少 | 漸進式補 Unit Test | | The Inspector | 測試私有方法 | 透過公開介面驗證行為 |