參數化測試(Parameterized Testing)

是什麼?

參數化測試(Parameterized Testing / Data-Driven Testing)是一種用同一段測試邏輯搭配多組輸入資料的技術,避免重複撰寫結構相同的測試。

ℹ️DRY 原則在測試中的應用

參數化測試是測試程式碼 DRY(Don't Repeat Yourself)的最佳工具。當你發現多個測試只有輸入和預期結果不同時,就是使用參數化測試的時機。

什麼時候用?

常見誤區

⚠️參數化測試的陷阱

  • 資料太多看不懂:當參數超過 4-5 個,測試就變得難以理解。考慮用物件或 named tuple 包裝
  • 錯誤訊息不清楚:失敗時只看到「第 7 組資料失敗」,加上 display name 讓每組資料有意義
  • 把不同邏輯硬塞進同一個參數化測試:如果不同組資料走的是不同程式路徑,拆成獨立測試
  • 忘了測試邊界值:參數化測試的威力在邊界值,不要只放 happy path 的資料

使用流程

1

辨識重複模式

發現多個測試只有輸入和預期不同,邏輯完全相同

2

抽取測試模板

寫一個通用的測試方法,用參數替代硬編碼的值

3

準備資料集

用框架提供的機制(InlineData、each、parametrize)餵入多組資料

4

加上顯示名稱

為每組資料加上有意義的描述,方便失敗時定位

5

執行並確認

框架會自動為每組資料執行一次測試,各自獨立報告結果

程式碼範例

C#(xUnit Theory + InlineData)

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

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

python
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

概念圖

參數化測試
一組邏輯
測試模板(邏輯)
資料集(輸入/預期)
xUnit Theory
InlineData / MemberData
Jest each
it.each
pytest parametrize

此圖展示參數化測試的核心結構:一個測試模板(邏輯)搭配一個資料集(輸入/預期),框架負責把兩者結合。三大框架各有自己的語法,但概念完全相同。

Bad vs Good 對比

csharp
// 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 是框架自動生成隨機資料來驗證一個「性質」永遠成立。兩者互補:

實戰補充

💡資深開發者的經驗

  • 邊界值是參數化測試的主戰場: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 |

你可能也想看

整合測試TDD 與 Legacy Code

按 ← → 鍵切換課程