單元測試最佳實踐
是什麼?
單元測試最佳實踐是一組經過社群驗證的原則和模式,幫助你寫出可讀、可維護、可信賴的測試程式碼。
ℹ️測試也是程式碼
測試程式碼和產品程式碼一樣重要。它需要被維護、被重構、被閱讀。對測試程式碼的品質要求不該低於產品程式碼。
核心觀念
- AAA 模式(Arrange-Act-Assert):每個測試分成三個區塊——準備(建立物件和資料)、執行(呼叫被測方法,通常只有一行)、驗證(檢查結果)。三個區塊之間用空行分隔,視覺上一目瞭然
- 測試命名慣例:
MethodName_Scenario_ExpectedResult,一看就知道在測什麼。例如CalculateTotal_EmptyCart_ReturnsZero。另一種流行的格式是should_returnZero_when_cartIsEmpty(BDD 風格) - 單一斷言原則:每個測試只驗證一件事,失敗時立刻知道哪裡出問題。例外:當多個 Assert 都在驗證同一個行為的不同面向時(如檢查回傳物件的多個欄位),放在一個測試裡是合理的
- FIRST 原則:Fast(單個測試在毫秒級完成)、Isolated(不依賴其他測試的執行順序或外部狀態)、Repeatable(在任何機器、任何時間跑結果都一樣)、Self-Validating(自動判斷通過或失敗,不需要人眼檢查 log)、Timely(與產品程式碼同步寫,不是事後補)
- 不要測試實作細節:測試行為(What),不要測試怎麼做(How)。判斷標準:如果你重構內部結構但外部行為不變,測試不該壞掉。如果壞了,代表你測了實作細節
常見誤區
⚠️壞測試的警訊
- 測試名稱是 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 |
| 核心好處 | 測試可讀、失敗時立刻知道原因 |
| 代價 | 需要紀律和團隊共識來維持一致性 |