返回自动化工程模块一条测试从需求到证据
用例保持一个清晰因果
退款服务的可控单元边界
Automation / Tutorial 11
Python 与 pytest 测试开发教程
从“会执行测试步骤”走到“能把业务规则写成可重复、可维护、可诊断的测试代码”。
10 个章节商城交易案例Python + pytest
01
先把测试场景翻译成代码结构
业务先于语法业务规则库存与优惠
测试数据输入与前置
执行动作下单或退款
分层断言响应与状态
失败证据日志和报告
商城下单业务链
用户购买商品时,系统先校验库存和优惠,再创建订单并发起支付;支付成功后扣减库存,取消或售后时进入退款。你会把这条链拆成小函数、小对象和独立用例。
HTTP 状态码只说明请求处理结果。下面的成功示例统一使用 200,但业务成功与否仍要检查响应体中的业务码、状态和金额。
02
掌握写测试真正需要的 Python
最小必要集业务数据与 Python 类型
| 类型 | 商城用途 | 示例 |
|---|---|---|
| dict | 订单请求、接口响应 | order["status"] |
| list | 商品明细、优惠券集合 | items[0] |
| Decimal | 金额计算 | Decimal("99.90") |
| dataclass | 结构固定的业务对象 | Order(id, amount) |
order.py
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class Order:
order_id: str
status: str
paid_amount: Decimal
def payable_amount(price: Decimal, quantity: int, discount: Decimal) -> Decimal:
if quantity <= 0:
raise ValueError("quantity must be positive")
return max(price * quantity - discount, Decimal("0.00"))学习顺序
- 先会变量、集合、条件、循环和函数。
- 再用类型标注表达输入输出,用 dataclass 表达订单。
- 金额必须使用 Decimal,避免浮点误差。
- 异常只表示调用不能正常继续,不要拿字符串代替业务状态。
03
写出第一条可执行 pytest 用例
Arrange Act AssertArrange准备价格和优惠
Act计算实付
Assert检查业务结果
test_order.py
from decimal import Decimal
from order import payable_amount
def test_当库存充足且优惠有效时_实付金额正确():
price = Decimal("100.00")
quantity = 2
discount = Decimal("20.00")
actual = payable_amount(price, quantity, discount)
assert actual == Decimal("180.00")运行命令
python -m venv .venv
# Windows PowerShell: .venv\Scripts\Activate.ps1
python -m pip install pytest
python -m pytest -q04
断言业务结果,也验证异常边界
失败要可读商城场景的可验证结果
| 场景 | 预期 |
|---|---|
| 当库存充足且优惠有效时,提交订单 | 返回 200,响应体 businessCode=SUCCESS,实付金额正确 |
| 当优惠券过期时,提交订单 | 返回 200,响应体拒绝优惠且不创建订单 |
| 当支付重复回调时,处理回调 | 返回 200,响应体确认已处理,订单只变更一次 |
| 当退款金额超过实付金额时,申请退款 | 返回 200,响应体业务失败且不生成退款单 |
响应体与异常断言
import pytest
def test_当下单成功时_检查响应体而不只看HTTP状态(order_client):
response = order_client.create({"skuId": "SKU-1", "quantity": 1})
assert response.status_code == 200
body = response.json()
assert body["businessCode"] == "SUCCESS"
assert body["data"]["status"] == "PENDING_PAYMENT"
def test_当购买数量为零时_拒绝计算():
with pytest.raises(ValueError, match="quantity must be positive"):
payable_amount(Decimal("10.00"), 0, Decimal("0.00"))05
用 Fixture 管理前置、清理和依赖
资源有生命周期Fixture 作用域选择
| scope | 复用范围 | 适合内容 |
|---|---|---|
| function | 每条用例 | 独立订单、库存 |
| module | 当前文件 | 只读商品目录 |
| session | 整次运行 | HTTP 客户端、报告配置 |
conftest.py
import pytest
@pytest.fixture
def available_sku(inventory_api):
sku_id = inventory_api.create_sku(stock=10)
yield sku_id
inventory_api.archive_sku(sku_id)
@pytest.fixture
def paid_order(order_api, available_sku):
order = order_api.create_and_pay(available_sku)
yield order
order_api.cancel_if_possible(order["orderId"])Fixture 提供状态,不替用例隐藏业务动作。测试退款可以依赖“已支付订单”,但“申请退款”仍应清楚写在测试正文中。
06
用参数化覆盖边界,不复制用例
一个规则一组数据优惠边界参数化
import pytest
from decimal import Decimal
@pytest.mark.parametrize(
("total", "coupon", "expected"),
[
("99.99", "20.00", "99.99"),
("100.00", "20.00", "80.00"),
("100.01", "20.00", "80.01"),
],
ids=["below-threshold", "at-threshold", "above-threshold"],
)
def test_当订单金额接近门槛时_优惠计算正确(total, coupon, expected):
actual = apply_coupon(Decimal(total), Decimal("100.00"), Decimal(coupon))
assert actual == Decimal(expected)参数化检查
- 每一组数据都验证同一条规则。
- ids 能直接说明失败边界。
- 不要把完全不同的业务流程塞进一个巨型参数表。
- 0、1、门槛前后、最大值和非法值都要有依据。
07
把数据驱动和数据工厂分开
默认有效,按需覆盖factories.py
from copy import deepcopy
DEFAULT_ORDER = {
"skuId": "SKU-1",
"quantity": 1,
"couponCode": None,
"addressId": "ADDR-1",
}
def order_payload(**overrides):
payload = deepcopy(DEFAULT_ORDER)
payload.update(overrides)
return payload
def test_当库存不足时_响应体返回明确原因(order_client):
response = order_client.create(order_payload(quantity=999))
assert response.status_code == 200
assert response.json()["businessCode"] == "OUT_OF_STOCK"外部 JSON/CSV 适合大量稳定数据;业务组合更适合工厂函数。数据来源不应把断言、流程和清理逻辑藏起来。
08
用 Mock 控制边界,不伪造整个世界
只替换外部依赖测试发起退款
退款规则真实执行
支付网关Mock 返回
断言金额与调用
支付网关 Mock
def test_当支付网关退款成功时_订单进入退款中(mocker):
gateway = mocker.Mock()
gateway.refund.return_value = {"success": True, "refundId": "R-1"}
service = RefundService(gateway)
result = service.apply(order_id="O-1", amount="80.00")
assert result.status == "REFUNDING"
gateway.refund.assert_called_once_with("O-1", "80.00")Mock 证明的是“你的代码怎样处理约定响应”,不能证明真实支付服务一定符合约定。下一阶段仍需要契约测试和真实联调。
09
让日志、失败信息和报告形成证据链
先能定位再谈重试pytest.ini
[pytest]
addopts = -ra --strict-markers
log_cli = true
log_cli_level = INFO
markers =
smoke: 核心交易冒烟
regression: 完整业务回归运行与报告
python -m pytest -m smoke -q
python -m pytest --junitxml=reports/junit.xml
python -m pytest --html=reports/report.html --self-contained-html失败时至少留下
- 用例名和参数 ID。
- 环境、版本和执行时间。
- 订单号、商品号、支付号或退款号。
- 响应状态、业务码和必要响应体。
- 调用栈与清理结果;日志中不得输出令牌或支付敏感信息。
10
组织项目并完成一次小型测试开发
可维护交付推荐目录
tests/
unit/ # 金额和状态规则
api/ # 订单、库存、支付、退款接口
conftest.py # 共享 Fixture
clients/ # HTTP 与业务客户端
factories/ # 测试数据工厂
pytest.ini
requirements-test.txt练习:完成商城交易测试包
- 为金额计算写正常、边界和异常测试。
- 用 Fixture 创建库存商品和已支付订单,并保证精确清理。
- 用参数化覆盖优惠门槛前、门槛值和门槛后。
- Mock 支付超时,验证订单不会被错误标记为已支付。
- 调用下单接口时检查 HTTP 200 和响应体业务结果。
- 为重复支付回调和超额退款各写一条“当……时,……”用例。
- 输出 JUnit 报告并故意制造失败,确认订单号和参数可定位。
- 按 unit、api、smoke 标记运行,记录每层耗时。
代码清楚
- 函数职责单一
- 金额使用 Decimal
- 类型表达业务
- 异常边界明确
用例可信
- 名称描述因果
- 数据互相隔离
- 断言业务结果
- Mock 边界明确
工程可运行
- 目录职责清楚
- 命令可重复
- 报告可追溯
- 敏感信息不入日志
你已经能用 pytest 表达业务规则。下一步把这些能力扩展到可维护的接口自动化工程。
继续学习接口自动化测试