iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0

今天要做什麼?

昨天我們學會了測試生命週期,解決了測試污染的問題。但現在面對一個新的挑戰:「要測試同一個函數的多組輸入輸出,難道要寫幾十個類似的測試嗎?」

想像一個場景:你要為數學工具庫的 is_prime 函數寫測試,需要驗證很多數字。如果每組都寫一個獨立的測試,程式碼會變得非常冗長且難以維護。今天我們要學習「參數化測試」,用優雅的方式處理大量測試資料。

學習目標

今天結束後,你將學會:

  • 理解參數化測試的概念與價值
  • 掌握 pytest.mark.parametrize 的使用
  • 學會設計有效的測試資料集
  • 理解資料驅動測試的最佳實踐

TDD 學習地圖

第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織
├── Day 05 - 測試生命週期
├── Day 06 - 參數化測試 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)

什麼是參數化測試? 📊

概念說明

參數化測試(Parameterized Testing)是一種測試技術,讓你能用同一組測試邏輯驗證多組不同的輸入資料。它有幾個別名:

  • 資料驅動測試(Data-Driven Testing)
  • 表格驅動測試(Table-Driven Testing)

傳統方式 vs 參數化測試

傳統方式需要為每個輸入寫一個獨立的測試,產生大量重複程式碼。參數化測試可以用一個測試函數處理多組資料:

# ✅ 參數化測試:乾淨、簡潔
@pytest.mark.parametrize("input,expected", [
    (2, True),
    (3, True),
    (5, True),
    (4, False),
    (6, False),
])
def test_is_prime(input, expected):
    assert is_prime(input) == expected

基本語法與用法 🔧

pytest.mark.parametrize 的基本語法

pytest 提供了 @pytest.mark.parametrize 裝飾器來實作參數化測試:

# 基本語法
@pytest.mark.parametrize("param1,param2", [
    (value1a, value1b),
    (value2a, value2b),
    (value3a, value3b),
])
def test_something(param1, param2):
    # 測試邏輯
    pass

# 使用變數名稱
@pytest.mark.parametrize("input,expected", [
    (10, 100),
    (20, 400),
    (30, 900),
])
def test_square(input, expected):
    assert square(input) == expected

實戰演練:建立質數檢測器

建立 src/math/prime_checker.py

def is_prime(n: int) -> bool:
    """檢查一個數字是否為質數"""
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    
    for i in range(3, int(n ** 0.5) + 1, 2):
        if n % i == 0:
            return False
    
    return True

建立 tests/day06/test_prime_checker.py

import pytest
from src.math.prime_checker import is_prime

class TestPrimeChecker:
    @pytest.mark.parametrize("input,expected", [
        (2, True), (3, True), (5, True), (7, True),
        (4, False), (6, False), (8, False), (9, False),
    ])
    def test_basic_prime_numbers(self, input, expected):
        assert is_prime(input) == expected

    @pytest.mark.parametrize("input,expected", [
        (0, False), (1, False), (-1, False),
    ])
    def test_edge_cases(self, input, expected):
        assert is_prime(input) == expected

    @pytest.mark.parametrize("input,expected", [
        (97, True),
        (101, True),
        (100, False),
    ])
    def test_large_numbers(self, input, expected):
        assert is_prime(input) == expected

使用 pytest.param 提高可讀性

為測試案例加上描述

當測試資料變複雜時,可以使用 pytest.param 加上描述:

@pytest.mark.parametrize("email,expected", [
    pytest.param("user@example.com", True, id="valid_email"),
    pytest.param("invalid-email", False, id="no_at_symbol"),
    pytest.param("@example.com", False, id="no_username"),
    pytest.param("user@", False, id="no_domain"),
    pytest.param("", False, id="empty_string"),
])
def test_email_validation(email, expected):
    assert is_valid_email(email) == expected

計算機測試範例 🧮

建立 src/math/calculator.py

class Calculator:
    def add(self, a: float, b: float) -> float:
        return a + b

    def subtract(self, a: float, b: float) -> float:
        return a - b

    def multiply(self, a: float, b: float) -> float:
        return a * b

    def divide(self, a: float, b: float) -> float:
        if b == 0:
            raise ValueError("Division by zero")
        return a / b

建立 tests/day06/test_calculator.py

import pytest
from src.math.calculator import Calculator

class TestCalculator:
    def setup_method(self):
        self.calculator = Calculator()

    @pytest.mark.parametrize("a,b,expected", [
        (1, 2, 3),
        (5, 3, 8),
        (-1, 1, 0),
    ])
    def test_addition(self, a, b, expected):
        assert self.calculator.add(a, b) == expected

    @pytest.mark.parametrize("a,b,expected", [
        (5, 3, 2),
        (10, 5, 5),
        (0, 5, -5),
    ])
    def test_subtraction(self, a, b, expected):
        assert self.calculator.subtract(a, b) == expected

    @pytest.mark.parametrize("a,b", [
        (5, 0),
        (10, 0),
        (-5, 0),
    ])
    def test_division_by_zero_raises_error(self, a, b):
        with pytest.raises(ValueError, match="Division by zero"):
            self.calculator.divide(a, b)

測試資料設計最佳實踐 🎯

設計原則

設計測試資料時要考慮:

  1. 覆蓋重要情境:正常情況、邊界值、錯誤格式
  2. 分組相關資料:將類似的測試案例組織在一起
  3. 使用清楚的描述:讓測試失敗時容易理解問題

實戰練習:字串工具測試

建立 src/utils/string_utils.py

def capitalize(text: str) -> str:
    """將字串首字母大寫"""
    if not text:
        return ''
    return text[0].upper() + text[1:].lower()

def truncate(text: str, max_length: int) -> str:
    """截斷字串至指定長度"""
    if len(text) <= max_length:
        return text
    return text[:max_length - 3] + '...'

建立 tests/day06/test_string_utils.py

import pytest
from src.utils.string_utils import capitalize, truncate

@pytest.mark.parametrize("input_text,expected", [
    ("hello", "Hello"),
    ("WORLD", "World"),
    ("typescript", "Typescript"),
    ("", ""),
])
def test_capitalize(input_text, expected):
    assert capitalize(input_text) == expected

@pytest.mark.parametrize("text,max_len,expected", [
    ("hello world", 5, "he..."),
    ("short", 10, "short"),
])
def test_truncate(text, max_len, expected):
    assert truncate(text, max_len) == expected

避免常見陷阱 ⚠️

常見錯誤

  1. 測試資料過多:選擇代表性資料,避免執行時間過長
  2. 描述不清楚:使用清楚的測試描述,便於理解測試目的
  3. 缺乏分組:將相關測試資料分組,提高測試組織性

好與壞的參數化測試

# ❌ 錯誤:描述不清、邏輯混雜
@pytest.mark.parametrize("op,a,b,expected", [
    ('add', 1, 2, 3),
    ('subtract', 5, 2, 3),
])
def test_operations(op, a, b, expected):
    # 複雜的條件邏輯
    pass

# ✅ 正確:清楚描述、單一職責
@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (4, 5, 9),
])
def test_addition(a, b, expected):
    assert add(a, b) == expected

今天學到什麼?

今天我們深入學習了參數化測試的概念和實際應用:

核心概念

  • 參數化測試:用同一組邏輯測試多組資料
  • 資料驅動測試:讓測試資料決定測試行為
  • @pytest.mark.parametrize:pytest 的參數化測試裝飾器

實用技巧

  • 陣列格式:簡單資料用陣列
  • pytest.param:複雜資料用描述
  • 描述性命名:讓測試結果易於理解

避免的陷阱

  • 資料過多:選擇代表性資料
  • 描述不清:使用清楚的測試描述
  • 缺乏分組:將相關測試資料分組

總結 🎆

參數化測試是提高測試效率和覆蓋率的強力工具:

  • 減少重複程式碼:一組邏輯測試多組資料
  • 提高測試覆蓋率:容易測試更多情境
  • 改善可維護性:集中管理測試資料
  • 增強可讀性:清楚的測試結構

記住:好的參數化測試資料設計是測試品質的關鍵。

明天我們將學習「測試替身基礎」,了解如何使用 Mock、Stub 等技術來隔離測試對象,讓測試更專注和可靠。


上一篇
Day 05 - 測試生命週期 🔄
下一篇
Day 07 - 測試替身基礎 🎭
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言