TDD 反模式(TDD Anti-Patterns)

是什麼?

TDD Anti-Pattern 是那些表面上遵循 TDD 流程,但實際上違反 TDD 精神的測試寫法。這些壞味道會讓測試變成負擔而非安全網。

⚠️測試債務

壞測試比沒有測試更危險——它們給你虛假的安全感,同時每次重構都瘋狂報錯,逼你花大量時間維護測試而非改善產品。

常見反模式

The Liar(說謊者)

The Giant(巨人)

The Mockery(Mock 狂魔)

Ice Cream Cone(冰淇淋甜筒)

The Inspector(窺探者)

辨識與修正流程

1

辨識壞味道

測試常常無故失敗?跑很慢?改一行壞十個?這些都是警訊

2

分類反模式

對照常見反模式清單,判斷屬於哪一種壞味道

3

評估影響

這個反模式造成多大的維護成本?優先修最痛的

4

重構測試

套用對應的修正策略,把測試改成健康的版本

5

驗證改善

確認測試仍然能抓到真正的 bug,且不再因重構而壞掉

程式碼範例:Bad vs Good

The Mockery — C# (xUnit)

csharp
// 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)

typescript
// 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)

python
# 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()

反模式關係圖

TDD Anti-Patterns
類型
The Liar (空洞斷言)
導致
The Giant (測試太大)
導致
The Mockery (過度 Mock)
導致
Ice Cream Cone (倒金字塔)
導致
The Inspector (測試實作)
導致
脆弱測試
執行緩慢
虛假安全感

此圖展示五種反模式分別導致的後果:The Liar 和 The Mockery 給你虛假安全感(以為有測試保護但其實沒有);The Giant 和 The Inspector 產生脆弱測試(稍微重構就全部壞掉);Ice Cream Cone 讓測試執行緩慢(CI 變成團隊的瓶頸)。

快速自我檢查清單

用以下問題檢查你的測試是否有反模式:

  1. 故意改壞一行產品程式碼,有測試會失敗嗎? 如果沒有 → 可能是 The Liar
  2. 一個測試失敗時,你能在 10 秒內知道原因嗎? 如果不能 → 可能是 The Giant
  3. 重構不改行為,測試會壞嗎? 如果會 → 可能是 The Inspector 或 The Mockery
  4. CI 跑完測試要多久? 如果超過 10 分鐘 → 可能是 Ice Cream Cone
  5. 每個測試的 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 | 測試私有方法 | 透過公開介面驗證行為 |

你可能也想看

BDD(行為驅動開發)TDD 實戰

按 ← → 鍵切換課程