优惠券写着“满 100 减 20”,订单恰好 100 元却没有抵扣。假设你让 AI 补测试,它给了十组数据,运行全部通过;仔细一看,订单金额只有 50、120、200,没有一条刚好等于 100。测试数量不少,关键规则却没有被检查。
这篇用一个虚构的结算函数演示 pytest 参数化测试,适合会运行 Python、正在给小工具或业务函数补测试的开发者与测试人员。你会得到完整代码、14 个用例、让 AI 补测试的提示词,以及一次“故意改错”的验证。示例不接支付系统,也不读取真实订单。
规则先定下来,期望值才有依据
本例约定:金额用整数“分”表示;会员打九折,结果按分四舍五入;优惠券门槛看折扣前金额,达到 10,000 分时减 2,000 分;优惠券在会员折扣之后扣除。负数报 ValueError,非整数报 TypeError;会员与优惠券标志只传布尔值。本例不覆盖运费、税费、叠加多券或订单状态。
因此,100 元会员订单再用券,应付 70 元;99.99 元会员订单即使选了券,也只打九折,得到 89.99 元。不要让 AI 根据函数现在输出什么来填写期望值:期望值来自这份规则与独立计算,测试才有机会发现实现错误。
工具方面,pytest 适合把多组输入写成一张参数表,还能断言异常;本例测试无需购买 AI 服务或调用模型 API。已有 Claude 可用于检查规则遗漏,Cursor 便于在项目中创建文件,GitHub Copilot 可辅助编辑代码。AI 功能可用性取决于账号,选你已有的即可,是否正确仍由测试验证。
把两份文件放进一个小目录
使用能安装当前 pytest 的 Python 环境。先新建 coupon-demo 目录,在其中创建虚拟环境。Windows PowerShell 可以用:
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install pytest
macOS / Linux 对应命令是 python3 -m venv .venv,然后运行 .venv/bin/python -m pip install pytest。这里直接使用虚拟环境中的解释器,不需要激活脚本,也不用为此调整 PowerShell 执行策略。后面的测试命令同样替换解释器路径。
在目录中保存 pricing.py。九折计算中的 + 5 用于非负整数金额的半入取整;它只服务于本例规则,不是所有货币和业务场景通用的舍入方案。
def payable_cents(subtotal_cents, member=False, coupon=False):
if type(subtotal_cents) is not int:
raise TypeError("subtotal_cents must be an integer")
if subtotal_cents < 0:
raise ValueError("subtotal_cents must not be negative")
amount = (subtotal_cents * 9 + 5) // 10 if member else subtotal_cents
if coupon and subtotal_cents >= 10000:
amount -= 2000
return amount先让 AI 列用例,再让它写断言
先把规则提供给 AI,不急着贴实现,让它列输入与应付金额。门槛需要低一分、恰好达到、高一分三组,会员与券需要组合,舍入需要小金额,异常需要单独处理。对 AI 的计算结果逐项复算,批准后再生成测试文件。
你在为一个 Python 结算函数设计测试。仅按下列规则列用例表:整数分金额,会员九折后按分四舍五入;券按折前金额判断,满10000分减2000分,并在会员折扣后扣除。负数报ValueError;非整数(包括True)报TypeError。标志只传布尔。覆盖9999、10000、10001分、会员与券组合、105与115分舍入、零金额和非法金额。每行写输入、期望值、对应规则及计算过程。先不要写代码;规则未说明的情况提出问题,不自行补充。
复核后再追加:“根据已批准的表生成 pytest 测试,使用 parametrize 和清晰的 ids;异常用 raises;不修改 pricing.py,不用待测函数计算 expected,不吞异常,不把失败标记成跳过。”提示词需要一起附上最终规则和批准的表,避免 AI 忘掉前文。
一张参数表,运行十四个独立用例
保存以下内容为 test_pricing.py,与 pricing.py 同目录。第一张表提供十组正常输入;三个非整数输入与一个负数再组成四个异常用例。pytest 会把参数组展开,失败时能看到 exact 或 stack 这样的标识。
import pytest
from pricing import payable_cents
@pytest.mark.parametrize(
"subtotal,member,coupon,expected",
[
(0, False, False, 0),
(9999, False, True, 9999),
(10000, False, True, 8000),
(10001, False, True, 8001),
(10000, True, False, 9000),
(10000, True, True, 7000),
(9999, True, True, 8999),
(105, True, False, 95),
(115, True, False, 104),
(10000, False, False, 10000),
],
ids=["zero", "below", "exact", "above", "member", "stack", "member-below", "round-105", "round-115", "plain"],
)
def test_payable(subtotal, member, coupon, expected):
assert payable_cents(subtotal, member, coupon) == expected
@pytest.mark.parametrize("bad", [True, 100.0, "10000"])
def test_reject_non_integer(bad):
with pytest.raises(TypeError):
payable_cents(bad)
def test_reject_negative():
with pytest.raises(ValueError):
payable_cents(-1)从 coupon-demo 目录运行:Windows 为 .\.venv\Scripts\python.exe -m pytest -q;macOS / Linux 为 .venv/bin/python -m pytest -q。正确保存后应看到 14 passed,耗时会因环境不同而变化。
故意弄坏一次,看看绿灯有没有用
在示例副本中,把 subtotal_cents >= 10000 临时改成 subtotal_cents > 10000,再次运行测试。本站在隔离环境使用 pytest 9.1.1 实际验证,结果为 2 failed, 12 passed:恰好满额的普通订单错误输出 10000,期望 8000;会员加券订单错误输出 9000,期望 7000。
这说明两个用例确实检查了“达到门槛也能用券”。恢复 >= 后,十四个用例再次通过。这个小实验只验证了这一种错误能被捕捉,不证明测试已经覆盖所有缺陷,也不要在生产结算代码里做实验。
失败时,可以把规则、相关函数、失败输入和断言给 AI,让它先解释差异,再提出最小修改。不要让它顺手把 expected 改成实际值;那会把发现问题的测试改成认可问题的测试。
几个看起来小、却会把测试做歪的细节
- 金额单位混用:100 分是 1 元,不是券门槛。接口名、提示词和用例表都保留 cents / 分。
- 布尔值混进金额:Python 中 bool 与 int 有继承关系,所以本例用严格类型判断并测试 True。实际产品若允许 Decimal 或数字字符串,需要另外制定转换规则。
- 舍入随手写:直接用
round()不是本例半入规则的替代写法。别为了得到“熟悉的代码”改变已确认的业务规则。 - 导入失败:检查两个文件是否同目录,从该目录用
python -m pytest运行;更复杂的 src 布局需要正确安装项目,不能靠随意追加路径掩盖问题。 - 全绿但没测到:文件名要符合
test_*.py等发现规则,并核对收集数量。退出码成功不是唯一验收项。
提交前检查:门槛上下与等号都覆盖;折扣顺序经人工确认;期望值独立计算;异常类型具体;人工改错能触发失败;恢复后重新运行,并保存测试命令与结果。迁移到真实项目时,还要增加实际业务中的撤销、重复结算等场景,不能把本文范围当成完整订单系统测试。
需要查语法时,可读 pytest 的 参数化说明与异常断言说明;虚拟环境见 Python venv 文档。把规则、用例表、实现和运行结果放在同一个评审上下文中,AI 才能帮你检查具体问题。