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 真正的杀手锏。