[测试技术] Pytest 入门与实战:fixture、参数化、标记与并行执行

发布时间:2026/7/23 21:09:19
[测试技术] Pytest 入门与实战:fixture、参数化、标记与并行执行 原创内容未获授权禁止转载、转发、抄袭。Python 自动化测试从几个脚本增长到完整测试集后通常会遇到数据重复准备、场景难以筛选、失败信息不直观和执行时间变长等问题。pytest 使用普通assert编写断言并通过 fixture、参数化、标记和插件机制组织测试适合单元测试、接口测试和回归测试。本文使用一个小型完整的内存“取消订单”案例演示 pytest 的核心用法并实际验证全量、冒烟和并行三种执行方式。pytest 适合解决什么问题能力典型用途断言重写使用普通assert失败时展示表达式中的实际值fixture准备测试数据、注入依赖并清理资源参数化用多组输入覆盖同一业务规则mark区分冒烟、回归、慢测试等测试集合插件扩展并行、报告、覆盖率等能力目前PyPI 上 pytest 最新稳定版为9.1.1发布于 2026 年 6 月 19 日要求 Python3.10。并行执行不是 pytest 内置能力本文使用 pytest-xdist3.8.0该插件要求 Python3.9因此本例最终以 pytest 的3.10为准。环境与项目结构示例目录如下pytest-demo/ ├── order_service.py ├── pytest.ini ├── requirements-test.txt └── tests/ ├── conftest.py ├── test_order_service.py └── test_order_status.py创建requirements-test.txt固定两个直接测试依赖的版本pytest9.1.1 pytest-xdist3.8.0macOS 和 Linux 创建并激活虚拟环境python3-mvenv .venvsource.venv/bin/activate python--versionpython-mpipinstall-rrequirements-test.txtWindows PowerShell 使用py-m venv.venv.\.venv\Scripts\Activate.ps1 python--version python-m pip install-r requirements-test.txt安装前应确认输出为 Python3.10。本文统一从项目根目录使用python -m pytest。与直接调用pytest命令相比这种方式会把当前目录加入sys.path示例中的order_service.py才能被稳定导入。创建pytest.ini[pytest] minversion 9.1 testpaths tests addopts -ra --strict-config --strict-markers markers smoke: 核心链路冒烟测试 regression: 完整回归测试testpaths限定测试目录避免在大型仓库中误收集脚本--strict-config和--strict-markers会让未知配置项、未注册或拼错的自定义标记直接报错。默认测试文件使用test_*.py或*_test.py函数使用test_*测试类使用Test*且不能定义__init__。准备被测代码本例包含以下规则只有待支付订单可以取消商品数量必须大于 0取消成功后订单状态变为已取消并释放对应库存已支付或不存在的订单取消失败且不能释放库存已取消订单再次取消失败且不能重复释放库存创建order_service.pyfromdataclassesimportdataclass,replacefromenumimportEnumclassOrderNotFoundError(LookupError):passclassOrderStatus(Enum):PENDINGpendingPAIDpaidCANCELLEDcancelleddefcan_cancel(self)-bool:returnselfisOrderStatus.PENDINGdataclass(frozenTrue)classOrder:order_id:intquantity:intstatus:OrderStatusdef__post_init__(self)-None:ifself.quantity0:raiseValueError(商品数量必须大于0)classOrderRepository:def__init__(self)-None:self._orders:dict[int,Order]{}defsave(self,order:Order)-None:self._orders[order.order_id]orderdeffind(self,order_id:int)-Order|None:returnself._orders.get(order_id)classOrderService:def__init__(self,repository:OrderRepository)-None:self._repositoryrepository self._released_quantity0propertydefreleased_quantity(self)-int:returnself._released_quantitydefadd(self,order:Order)-None:self._repository.save(order)defcancel(self,order_id:int)-Order:orderself._repository.find(order_id)iforderisNone:raiseOrderNotFoundError(订单不存在)ifnotorder.status.can_cancel():raiseValueError(当前状态不允许取消)cancelledreplace(order,statusOrderStatus.CANCELLED)self._repository.save(cancelled)self._released_quantityorder.quantityreturncancelledOrderRepository是本例可观察的存储边界测试可以验证取消失败后订单没有被错误修改。正式项目更适合使用src目录布局并安装项目包真实取消流程如果同时修改数据库订单和外部库存还需要事务或幂等补偿不能用这个内存示例推导生产一致性。fixture统一准备测试对象创建tests/conftest.pyimportpytestfromorder_serviceimportOrderRepository,OrderServicepytest.fixturedeforder_repository()-OrderRepository:returnOrderRepository()pytest.fixturedeforder_service(order_repository:OrderRepository)-OrderService:returnOrderService(order_repository)测试函数只要声明同名参数pytest 就会自动注入 fixture不需要导入conftest.py。order_service依赖order_repository同一条测试即使同时声明这两个参数也会复用同一个仓库实例。fixture 默认作用域为function每条用例都会创建独立仓库测试结束后对象直接丢弃不需要额外清理。数据库连接、临时服务等外部资源应使用yield并在其后执行关闭操作测试函数失败时清理代码仍会执行但 fixture 在到达yield前失败时不会执行后半段。conftest.py中的 fixture 对其所在目录及子目录生效重名 fixture 可能被下层目录覆盖。可用作用域包括function、class、module、package和session。扩大作用域能减少初始化次数但也会增加状态污染风险不应只为提速就共享可变对象。断言正常与异常路径创建tests/test_order_service.pyimportpytestfromorder_serviceimport(Order,OrderNotFoundError,OrderRepository,OrderService,OrderStatus,)pytest.mark.regressionpytest.mark.parametrize(quantity,[pytest.param(0,idzero),pytest.param(-1,idnegative),],)deftest_order_quantity_must_be_positive(quantity:int)-None:withpytest.raises(ValueError,match商品数量必须大于0):Order(1000,quantity,OrderStatus.PENDING)pytest.mark.smokepytest.mark.regressiondeftest_cancel_pending_order_releases_inventory(order_service:OrderService,order_repository:OrderRepository,)-None:order_service.add(Order(1001,2,OrderStatus.PENDING))actualorder_service.cancel(1001)assertactual.order_id1001assertactual.statusisOrderStatus.CANCELLED storedorder_repository.find(1001)assertstoredisnotNoneassertstored.statusisOrderStatus.CANCELLEDassertorder_service.released_quantity2pytest.mark.regressiondeftest_paid_order_does_not_release_inventory(order_service:OrderService,order_repository:OrderRepository,)-None:order_service.add(Order(1002,1,OrderStatus.PAID))withpytest.raises(ValueError,match当前状态不允许取消):order_service.cancel(1002)storedorder_repository.find(1002)assertstoredisnotNoneassertstored.statusisOrderStatus.PAIDassertorder_service.released_quantity0pytest.mark.regressiondeftest_repeated_cancel_does_not_release_inventory_twice(order_service:OrderService,order_repository:OrderRepository,)-None:order_service.add(Order(1003,2,OrderStatus.PENDING))order_service.cancel(1003)withpytest.raises(ValueError,match当前状态不允许取消):order_service.cancel(1003)storedorder_repository.find(1003)assertstoredisnotNoneassertstored.statusisOrderStatus.CANCELLEDassertorder_service.released_quantity2pytest.mark.regressiondeftest_missing_order_does_not_release_inventory(order_service:OrderService,)-None:withpytest.raises(OrderNotFoundError)asexc_info:order_service.cancel(9999)asserttype(exc_info.value)isOrderNotFoundErrorassertorder_service.released_quantity0pytest 会重写测试模块中的assert失败时展示参与比较的值不需要额外断言类。pytest.raises()会接受目标异常的子类要求类型完全一致时可像不存在订单的用例一样断言type(exc_info.value)。match按正则表达式匹配异常文本只应在消息属于接口或产品契约时使用如果消息中包含[]、()等正则字符可使用re.escape()。正常用例同时检查返回对象、仓库状态和库存释放量防止实现只返回新对象却漏掉持久化。异常用例则读取仓库确认状态未被修改并检查released_quantity防止失败路径错误释放库存。参数化覆盖状态规则创建tests/test_order_status.pyimportpytestfromorder_serviceimportOrderStatuspytest.mark.regressionpytest.mark.parametrize((status,expected),[pytest.param(OrderStatus.PENDING,True,idpending),pytest.param(OrderStatus.PAID,False,idpaid),pytest.param(OrderStatus.CANCELLED,False,idcancelled),],)deftest_only_pending_status_can_be_cancelled(status:OrderStatus,expected:bool,)-None:assertstatus.can_cancel()isexpected每个参数组合都会成为独立测试id会出现在收集结果和失败报告中比默认对象表示更容易定位。测试数据应直接给出期望值不要在参数生成逻辑中复制生产代码的判断条件。pytest 会把参数对象原样传给测试不会自动复制列表、字典等可变对象。如果测试会修改参数应在用例内复制或改用 fixture 创建避免前一个参数用例影响后一个。当参数只表示输入和期望值时使用pytest.mark.parametrize当每组参数还需要创建并清理资源时使用带params的参数化 fixture 更合适。标记与测试筛选本例注册了smoke和regression两个标记可以按测试范围执行同一条测试可以拥有多个 mark。本例的正常取消既属于冒烟范围也属于完整回归范围因此同时标记为smoke和regression。# 执行全部测试python-mpytest# 只执行冒烟测试python-mpytest-msmoke# 执行回归但排除冒烟测试python-mpytest-mregression and not smoke# 按名称表达式筛选python-mpytest-kcancel and not missing-m匹配 mark-k匹配测试节点名称、父级名称和关键字两者可以组合使用最终只执行同时满足两个表达式的测试。长期稳定的测试类型适合使用 mark临时定位某个模块或函数时使用-k。使用 pytest-xdist 并行执行安装 pytest-xdist 后通过-n指定 worker 数# 使用两个进程并行执行python-mpytest-n2# 根据可用 CPU 自动选择进程数python-mpytest-nauto# 同一模块的测试分配给同一 workerpython-mpytest-n2--distloadscopepytest-xdist 使用多个 worker 进程分发测试。每个进程拥有独立内存但账号、数据库、文件和远程环境仍可能冲突测试数据需要按 worker 或用例隔离。测试可注入 xdist 提供的worker_idfixture使用gw0、gw1等值生成不同的数据前缀。session作用域 fixture 也是每个 worker 各执行一次必须全局只执行一次的初始化应放在测试运行前或通过文件锁、数据库锁等跨进程机制保护。--dist loadscope会把同一模块的函数或同一测试类的方法交给同一 worker有利于复用模块、类级 fixture但负载可能不如默认调度均匀。无论使用哪种策略都不应依赖收集顺序或完成顺序。并行存在进程启动和调度开销短小测试可能比串行更慢。先在 CI 中记录耗时和失败率再决定 worker 数依赖执行顺序或共享全局状态的测试应先拆除依赖再开启并行。执行与报告本文示例在 Python3.12.13、pytest9.1.1、pytest-xdist3.8.0环境中实际执行执行方式结果python -m pytest -q9 条通过python -m pytest -q -m smoke1 条通过8 条未选择python -m pytest -q -n 29 条通过CI 中可生成 JUnit XML由流水线归档python-mpytest--durations10--junitxmlreports/pytest.xml--durations10会列出最慢的 10 个 setup、call 或 teardown 阶段适合判断慢点来自测试步骤还是 fixture。失败排查时可使用-x在首次失败后停止或使用--maxfail3限制失败数修复后用--lf只执行上次失败的用例。不要在团队默认配置中长期启用--pdb否则无人值守的 CI 会停在交互调试状态。--lf依赖项目根目录下的.pytest_cache。虚拟环境、.pytest_cache、__pycache__和本地reports目录应加入.gitignoreCI 报告则通过流水线制品归档。常见问题问题处理方式没有收集到测试检查文件、函数命名和testpaths配置CI 提示 no tests ranpytest 未收集到测试时退出码为 5应修正收集规则不要直接忽略失败fixture not found检查 fixture 名称及conftest.py所在目录Unknown mark在配置文件中注册 mark并开启--strict-markers参数化用例名称难懂使用pytest.param(..., id...)设置业务化名称单独执行通过并行失败排查共享账号、文件、数据库记录和端口冲突session fixture 初始化多次xdist 下每个 worker 都有独立测试会话本地能导入CI 导入失败固定从项目根目录执行并使用虚拟环境或正式安装项目包工程实践建议一个 fixture 只负责一种资源的准备与清理不在其中隐藏核心测试步骤每条测试独立创建数据并能脱离其他用例单独执行参数化只承载同一规则的多组输入不把不同业务流程塞进一张数据表对异常路径同时验证异常、状态和不应发生的副作用在配置中注册 mark、固定插件版本并让本地与 CI 使用同一命令并行执行前先解决数据隔离再根据 CI 实测结果调整 worker 数总结pytest 的核心是让测试依赖、数据和执行范围保持清晰用 fixture 管理上下文用参数化覆盖规则用 mark 选择测试集并在用例能够独立执行、数据已经隔离后再使用 xdist 并行。