參數化測試(Parameterized Testing)
是什麼?
參數化測試(Parameterized Testing / Data-Driven Testing)是一種用同一段測試邏輯搭配多組輸入資料的技術,避免重複撰寫結構相同的測試。
ℹ️DRY 原則在測試中的應用
參數化測試是測試程式碼 DRY(Don't Repeat Yourself)的最佳工具。當你發現多個測試只有輸入和預期結果不同時,就是使用參數化測試的時機。
什麼時候用?
- 多個測試的邏輯完全相同,只有輸入值和預期結果不同
- 邊界值測試:需要測試大量的邊界條件(0、負數、最大值、空字串)
- 規則驗證:如表單驗證、業務規則,每條規則有多種合法/非法輸入
- 格式轉換:如日期格式、幣別轉換,輸入輸出有明確對應
常見誤區
⚠️參數化測試的陷阱
- 資料太多看不懂:當參數超過 4-5 個,測試就變得難以理解。考慮用物件或 named tuple 包裝
- 錯誤訊息不清楚:失敗時只看到「第 7 組資料失敗」,加上 display name 讓每組資料有意義
- 把不同邏輯硬塞進同一個參數化測試:如果不同組資料走的是不同程式路徑,拆成獨立測試
- 忘了測試邊界值:參數化測試的威力在邊界值,不要只放 happy path 的資料
使用流程
辨識重複模式
發現多個測試只有輸入和預期不同,邏輯完全相同
抽取測試模板
寫一個通用的測試方法,用參數替代硬編碼的值
準備資料集
用框架提供的機制(InlineData、each、parametrize)餵入多組資料
加上顯示名稱
為每組資料加上有意義的描述,方便失敗時定位
執行並確認
框架會自動為每組資料執行一次測試,各自獨立報告結果
程式碼範例
C#(xUnit Theory + InlineData)
public class EmailValidatorTests
{
// 參數化測試:一組邏輯,多組資料
[Theory]
[InlineData("user@example.com", true)]
[InlineData("admin@company.org", true)]
[InlineData("", false)]
[InlineData("not-an-email", false)]
[InlineData("missing@", false)]
[InlineData("@no-local.com", false)]
public void IsValid_VariousInputs_ReturnsExpected(
string email, bool expected)
{
var validator = new EmailValidator();
var result = validator.IsValid(email);
Assert.Equal(expected, result);
}
// 使用 MemberData 傳入複雜資料
[Theory]
[MemberData(nameof(DiscountTestData))]
public void CalculateDiscount_VariousTiers_ReturnsExpected(
decimal price, string tier, decimal expected)
{
var calc = new PriceCalculator();
Assert.Equal(expected, calc.CalculateDiscount(price, tier));
}
public static IEnumerable<object[]> DiscountTestData =>
new List<object[]>
{
new object[] { 100m, "regular", 100m },
new object[] { 100m, "silver", 90m },
new object[] { 100m, "gold", 80m },
new object[] { 100m, "vip", 70m },
};
}TypeScript(Jest each)
describe("EmailValidator", () => {
// it.each — Jest 的參數化測試語法
it.each([
["user@example.com", true],
["admin@company.org", true],
["", false],
["not-an-email", false],
["missing@", false],
["@no-local.com", false],
])("isValid(%s) returns %s", (email, expected) => {
const validator = new EmailValidator();
expect(validator.isValid(email as string)).toBe(expected);
});
// 使用物件格式,提升可讀性
it.each([
{ price: 100, tier: "regular", expected: 100 },
{ price: 100, tier: "silver", expected: 90 },
{ price: 100, tier: "gold", expected: 80 },
{ price: 100, tier: "vip", expected: 70 },
])(
"calculates $tier discount: $price → $expected",
({ price, tier, expected }) => {
const calc = new PriceCalculator();
expect(calc.calculateDiscount(price, tier)).toBe(expected);
}
);
});Python(pytest parametrize)
import pytest
from email_validator import EmailValidator
from price_calculator import PriceCalculator
# pytest.mark.parametrize — Python 的參數化測試
@pytest.mark.parametrize("email,expected", [
("user@example.com", True),
("admin@company.org", True),
("", False),
("not-an-email", False),
("missing@", False),
("@no-local.com", False),
])
def test_is_valid_various_inputs(email, expected):
validator = EmailValidator()
assert validator.is_valid(email) == expected
# 使用 ids 參數增加可讀性
@pytest.mark.parametrize("price,tier,expected", [
(100, "regular", 100),
(100, "silver", 90),
(100, "gold", 80),
(100, "vip", 70),
], ids=["regular-no-discount", "silver-10%", "gold-20%", "vip-30%"])
def test_calculate_discount_various_tiers(price, tier, expected):
calc = PriceCalculator()
assert calc.calculate_discount(price, tier) == expected概念圖
此圖展示參數化測試的核心結構:一個測試模板(邏輯)搭配一個資料集(輸入/預期),框架負責把兩者結合。三大框架各有自己的語法,但概念完全相同。
Bad vs Good 對比
// BAD: 重複的測試邏輯,只有資料不同
[Fact]
public void IsValid_EmptyString_ReturnsFalse()
{
Assert.False(new EmailValidator().IsValid(""));
}
[Fact]
public void IsValid_NoAtSign_ReturnsFalse()
{
Assert.False(new EmailValidator().IsValid("not-an-email"));
}
[Fact]
public void IsValid_MissingDomain_ReturnsFalse()
{
Assert.False(new EmailValidator().IsValid("missing@"));
}
// GOOD: 參數化測試,一組邏輯、多組資料
[Theory]
[InlineData("", false)]
[InlineData("not-an-email", false)]
[InlineData("missing@", false)]
[InlineData("user@example.com", true)]
public void IsValid_VariousInputs_ReturnsExpected(string email, bool expected)
{
Assert.Equal(expected, new EmailValidator().IsValid(email));
}和 Property-Based Testing 的關係
參數化測試是你選資料,Property-Based Testing 是框架自動生成隨機資料來驗證一個「性質」永遠成立。兩者互補:
- 參數化測試:適合邊界值和已知的特殊案例(null、空字串、最大值)
- Property-Based Testing:適合發現你沒想到的邊界案例。工具:C# 用 FsCheck、JS/TS 用 fast-check、Python 用 Hypothesis
實戰補充
💡資深開發者的經驗
- 邊界值是參數化測試的主戰場:0、-1、null、空字串、最大值、剛好超過限制——把所有邊界值一次列出來。
- Display Name 是救命稻草:失敗時看到「InlineData(line 7)」沒有幫助。xUnit 用
[Theory]+MemberData,Jest 用模板字串,pytest 用ids參數。 - 超過 5 個參數就該重構:如果每組資料有太多欄位,用 Test Data Object 包裝,或拆成多個參數化測試。
- 和 Property-Based Testing 互補:參數化測試是你選資料,Property-Based Testing(如 FsCheck、fast-check)是框架自動生成資料。
理解測驗
🤔 什麼時候最適合使用參數化測試?
🤔 以下哪個不是參數化測試的常用框架語法?
🤔 參數化測試中加上 display name 的主要目的是什麼?
重點整理
💡一句話記住
口訣:「邏輯寫一次、資料列一排,框架自己跑完全部」—— 邊界值一網打盡。
| 概念 | 說明 |
|------|------|
| 核心思想 | 同一段邏輯,搭配多組輸入/預期資料 |
| C# 語法 | [Theory] + [InlineData] / [MemberData] |
| TypeScript 語法 | it.each([...]) |
| Python 語法 | @pytest.mark.parametrize |
| 核心好處 | 消除重複、一次覆蓋大量邊界值 |
| 代價 | 參數太多會降低可讀性,需要加 display name |