返回自动化工程模块
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 Assert
用例保持一个清晰因果
Arrange准备价格和优惠
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 -q
04

断言业务结果,也验证异常边界

失败要可读

商城场景的可验证结果

场景预期
当库存充足且优惠有效时,提交订单返回 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

练习:完成商城交易测试包

  1. 为金额计算写正常、边界和异常测试。
  2. 用 Fixture 创建库存商品和已支付订单,并保证精确清理。
  3. 用参数化覆盖优惠门槛前、门槛值和门槛后。
  4. Mock 支付超时,验证订单不会被错误标记为已支付。
  5. 调用下单接口时检查 HTTP 200 和响应体业务结果。
  6. 为重复支付回调和超额退款各写一条“当……时,……”用例。
  7. 输出 JUnit 报告并故意制造失败,确认订单号和参数可定位。
  8. 按 unit、api、smoke 标记运行,记录每层耗时。

代码清楚

  • 函数职责单一
  • 金额使用 Decimal
  • 类型表达业务
  • 异常边界明确

用例可信

  • 名称描述因果
  • 数据互相隔离
  • 断言业务结果
  • Mock 边界明确

工程可运行

  • 目录职责清楚
  • 命令可重复
  • 报告可追溯
  • 敏感信息不入日志

你已经能用 pytest 表达业务规则。下一步把这些能力扩展到可维护的接口自动化工程。

继续学习接口自动化测试