4、参数化测试:让测试用例“活”起来

大家好,我是你们的老朋友。今天我们来聊聊参数化测试。

说实话,我刚开始写自动化测试那会儿,最烦的就是重复代码。一个登录功能,测3种账号、5种密码、2种验证码……光写测试函数就写了十几个。后来我发现了参数化测试,嗯,真香。

4.1 参数化的意义:为什么我们需要它?

参数化测试,说白了就是“一套逻辑,多组数据”。你写一个测试函数,给它喂不同的输入,它就能自动跑出不同的结果。

它的核心价值有三点:

  • 减少重复代码:一个函数搞定几十个测试场景
  • 提高覆盖率:边界值、异常值、正常值,一键覆盖
  • 便于维护:改逻辑只改一处,加数据只加一行

我在项目中遇到过最典型的场景:一个支付接口,需要测试人民币、美元、欧元、日元四种货币,每种货币还要测正常金额、0元、负数、超大金额。如果没有参数化,你想想看,光写函数名就能把人逼疯。

核心观点:参数化不是“锦上添花”,而是“雪中送炭”。当你的测试用例超过10个时,参数化就是刚需。

4.2 使用 @pytest.mark.parametrize:最优雅的方式

在 pytest 里,参数化最常用的就是 @pytest.mark.parametrize 这个装饰器。我个人习惯把它叫做“测试用例的加速器”。

基本用法:

import pytest

# 单个参数
@pytest.mark.parametrize("username", ["admin", "user1", "guest"])
def test_login_username(username):
    print(f"测试用户名:{username}")
    assert len(username) > 0

# 多个参数
@pytest.mark.parametrize("username, password, expected", [
    ("admin", "123456", True),
    ("user1", "wrong", False),
    ("guest", "", False)
])
def test_login(username, password, expected):
    result = login(username, password)
    assert result == expected

你看,一个函数就覆盖了3种场景。如果再加一种,只需要在列表里加一行数据就行。

进阶技巧:组合参数化

有时候我们需要测试多个参数的笛卡尔积。比如测试一个搜索功能,既要测关键词,又要测搜索类型。

@pytest.mark.parametrize("keyword", ["手机", "电脑", "耳机"])
@pytest.mark.parametrize("search_type", ["精确", "模糊", "前缀"])
def test_search(keyword, search_type):
    print(f"搜索关键词:{keyword},类型:{search_type}")
    # 这里会生成 3 × 3 = 9 个测试用例

为什么会这样?因为 pytest 会把两个装饰器做笛卡尔积。我个人觉得这个特性特别适合做“多维度组合测试”。

小技巧:如果参数太多,建议把数据单独定义成一个列表变量,这样代码更清晰。

4.3 从外部文件读取参数:让数据与代码分离

写死在代码里的参数,虽然方便,但不够灵活。我建议把测试数据放到外部文件里,比如 JSON、YAML、CSV。

为什么这么做?

  • 测试人员可以直接改数据文件,不用碰代码
  • 不同环境(测试、预发布)可以加载不同数据
  • 数据量大时,文件比代码更易管理

示例:从 JSON 文件读取参数

# test_data.json
[
  {"username": "admin", "password": "123456", "expected": True},
  {"username": "user1", "password": "wrong", "expected": False},
  {"username": "guest", "password": "", "expected": False}
]
# test_login.py
import json
import pytest

def load_test_data():
    with open("test_data.json", "r", encoding="utf-8") as f:
        return json.load(f)

@pytest.mark.parametrize("data", load_test_data())
def test_login_from_json(data):
    result = login(data["username"], data["password"])
    assert result == data["expected"]

示例:从 CSV 文件读取参数

# test_data.csv
username,password,expected
admin,123456,True
user1,wrong,False
guest,,False
import csv
import pytest

def load_csv_data():
    with open("test_data.csv", "r") as f:
        reader = csv.DictReader(f)
        return list(reader)

@pytest.mark.parametrize("row", load_csv_data())
def test_login_from_csv(row):
    result = login(row["username"], row["password"])
    expected = row["expected"] == "True"
    assert result == expected

避坑指南:我曾经在 CSV 文件里把布尔值写成了字符串 "True",结果断言一直失败。后来才发现 CSV 默认把所有内容都当字符串处理。记得做类型转换!

4.4 动态生成测试用例:让测试“自己长出来”

有时候,测试数据不是固定的,而是需要根据某些条件动态生成。比如从数据库里查出一批用户,然后对每个用户执行相同的测试。

方法一:使用 pytest_generate_tests 钩子

def pytest_generate_tests(metafunc):
    if "dynamic_user" in metafunc.fixturenames:
        # 模拟从数据库获取用户列表
        users = ["user_001", "user_002", "user_003"]
        metafunc.parametrize("dynamic_user", users)

def test_dynamic(dynamic_user):
    print(f"动态生成的测试用例:{dynamic_user}")
    assert len(dynamic_user) > 0

方法二:使用 fixture 返回参数化数据

import pytest

@pytest.fixture(params=["chrome", "firefox", "edge"])
def browser(request):
    return request.param

def test_browser(browser):
    print(f"测试浏览器:{browser}")
    # 这里会自动生成3个测试用例

我个人更喜欢第二种方式,因为它更直观。但如果你需要根据外部条件(比如环境变量)动态决定测试用例的数量,那第一种方式更灵活。

动态生成的核心逻辑:

外部数据源 动态加载 参数化生成 执行

你想想看,如果测试数据是从数据库实时拉取的,那每次运行测试时,测试用例的数量和内容都可能不同。这就是“动态生成”的魅力。

4.5 实战经验总结

最后,我分享几个实战中的小经验:

场景 推荐方式 原因
参数少且固定 @pytest.mark.parametrize 简单直接,代码即文档
参数多且变化频繁 外部文件(JSON/CSV) 数据与代码分离,方便维护
参数来自外部系统 动态生成(钩子/fixture) 灵活应对实时数据
需要组合测试 多个 @parametrize 叠加 自动生成笛卡尔积

一句话总结:参数化测试不是“要不要用”的问题,而是“怎么用得更好”的问题。从硬编码到文件读取,再到动态生成,每一步都是测试架构的进化。

好了,这一章就到这里。下一章我们聊聊“fixture 的高级用法”,那才是 pytest 真正的杀手锏。