單元測試最佳實踐

是什麼?

單元測試最佳實踐是一組經過社群驗證的原則和模式,幫助你寫出可讀、可維護、可信賴的測試程式碼。

ℹ️測試也是程式碼

測試程式碼和產品程式碼一樣重要。它需要被維護、被重構、被閱讀。對測試程式碼的品質要求不該低於產品程式碼。

核心觀念

常見誤區

⚠️壞測試的警訊

  • 測試名稱是 Test1、Test2:沒有描述性的名稱,失敗時完全不知道在幹嘛
  • 一個測試驗證十件事:失敗時不知道是哪個斷言出問題
  • 測試之間有依賴順序:測試 B 依賴測試 A 的結果,一旦 A 失敗全部連鎖爆炸
  • 在測試中寫複雜邏輯:測試本身不該有 if-else 或迴圈,應該是直線式的流程

AAA 模式流程

1

Arrange(準備)

建立受測物件、設定測試資料、準備 Test Double

2

Act(執行)

呼叫受測方法,通常只有一行

3

Assert(驗證)

驗證結果是否符合預期,理想上只有一個 Assert

程式碼範例

C#(xUnit)

csharp
public class OrderTests
{
    // 好的命名:Method_Scenario_Expected
    [Fact]
    public void CalculateTotal_WithDiscount_ReturnsDiscountedPrice()
    {
        // Arrange
        var order = new Order();
        order.AddItem(new Item("Book", 100m));
        var discount = 0.1m; // 10% off
 
        // Act
        var total = order.CalculateTotal(discount);
 
        // Assert
        Assert.Equal(90m, total);
    }
 
    [Fact]
    public void CalculateTotal_EmptyOrder_ReturnsZero()
    {
        // Arrange
        var order = new Order();
 
        // Act
        var total = order.CalculateTotal(0m);
 
        // Assert
        Assert.Equal(0m, total);
    }
 
    // 壞的範例(不要這樣寫)
    // [Fact]
    // public void Test1()  ← 名字沒意義
    // {
    //     var order = new Order();
    //     order.AddItem(new Item("A", 100));
    //     Assert.Equal(100, order.CalculateTotal(0));
    //     order.AddItem(new Item("B", 200));  ← 一個測試做太多事
    //     Assert.Equal(300, order.CalculateTotal(0));
    //     Assert.NotNull(order.Items);  ← 跟主題無關的斷言
    // }
}

TypeScript(Jest)

typescript
describe("Order", () => {
  // 好的命名:清楚描述場景與預期
  describe("calculateTotal", () => {
    it("returns discounted price when discount is applied", () => {
      // Arrange
      const order = new Order();
      order.addItem({ name: "Book", price: 100 });
      const discount = 0.1;
 
      // Act
      const total = order.calculateTotal(discount);
 
      // Assert
      expect(total).toBe(90);
    });
 
    it("returns zero for empty order", () => {
      // Arrange
      const order = new Order();
 
      // Act
      const total = order.calculateTotal(0);
 
      // Assert
      expect(total).toBe(0);
    });
  });
});

Python(pytest)

python
class TestOrder:
    # 好的命名:method_scenario_expected
    def test_calculate_total_with_discount_returns_discounted_price(self):
        # Arrange
        order = Order()
        order.add_item(Item("Book", 100))
        discount = 0.1
 
        # Act
        total = order.calculate_total(discount)
 
        # Assert
        assert total == 90
 
    def test_calculate_total_empty_order_returns_zero(self):
        # Arrange
        order = Order()
 
        # Act
        total = order.calculate_total(0)
 
        # Assert
        assert total == 0

觀念圖

AAA 模式
搭配使用
命名慣例
共同提升品質
FIRST 原則
毫秒級完成
Fast (快速)
Isolated (隔離)
Repeatable (可重複)
Self-Validating (自驗證)
Timely (及時)

此圖展示三大核心實踐的關係:AAA 模式管測試的結構、命名慣例管測試的可讀性、FIRST 原則管測試的品質。三者互相搭配,缺一不可。

Bad vs Good 對比

csharp
// BAD: 多個不相關斷言、名稱無意義、沒有 AAA 結構
[Fact]
public void Test1()
{
    var order = new Order();
    order.AddItem(new Item("A", 100));
    Assert.Equal(100, order.CalculateTotal(0));
    order.AddItem(new Item("B", 200));
    Assert.Equal(300, order.CalculateTotal(0));
    Assert.NotNull(order.Items);
    Assert.True(order.Items.Count > 0);
}
 
// GOOD: 一個測試一個概念、AAA 清晰、命名即文件
[Fact]
public void CalculateTotal_TwoItems_ReturnsSumOfPrices()
{
    // Arrange
    var order = new Order();
    order.AddItem(new Item("Book", 100m));
    order.AddItem(new Item("Pen", 50m));
 
    // Act
    var total = order.CalculateTotal(discount: 0m);
 
    // Assert
    Assert.Equal(150m, total);
}

實戰補充

💡資深開發者的經驗

  • AAA 之間用空行分隔:視覺上一目瞭然,不需要寫註解標記 Arrange/Act/Assert。
  • 測試名稱是最好的文件:當測試失敗時,你第一眼看到的就是測試名稱。投資在好名字上,未來的你會感謝自己。
  • Builder Pattern 管理測試資料:當 Arrange 區塊太長時,用 Test Data Builder 來簡化。例如 new OrderBuilder().WithItem("Book", 100).WithDiscount(0.1).Build()
  • 一個測試一個失敗原因:如果你需要問「這個測試到底在測什麼?」,代表它測太多了。

理解測驗

🤔 AAA 模式中的三個 A 分別代表什麼?

🤔 以下哪個測試名稱最符合最佳實踐?

🤔 FIRST 原則中的 I(Isolated)是什麼意思?

重點整理

💡一句話記住

口訣:「三 A 排排站、名字說故事、FIRST 保品質」—— 好測試讓未來的你說聲謝謝。

| 概念 | 說明 | |------|------| | AAA 模式 | Arrange-Act-Assert,每個測試三段式結構 | | 命名慣例 | MethodName_Scenario_Expected,名字即文件 | | 單一斷言 | 每個測試只驗證一件事 | | FIRST | Fast / Isolated / Repeatable / Self-Validating / Timely | | 核心好處 | 測試可讀、失敗時立刻知道原因 | | 代價 | 需要紀律和團隊共識來維持一致性 |

你可能也想看

Test Doubles測試金字塔

按 ← → 鍵切換課程