尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

[测试技术] Pytest 入门与实战:fixture、参数化、标记与并行执行
📅 发布时间:2026/7/23 21:23:47

原创内容,未获授权禁止转载、转发、抄袭。

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,固定两个直接测试依赖的版本:

pytest==9.1.1 pytest-xdist==3.8.0

macOS 和 Linux 创建并激活虚拟环境:

python3-mvenv .venvsource.venv/bin/activate python--versionpython-mpipinstall-rrequirements-test.txt

Windows 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.py:

fromdataclassesimportdataclass,replacefromenumimportEnumclassOrderNotFoundError(LookupError):passclassOrderStatus(Enum):PENDING="pending"PAID="paid"CANCELLED="cancelled"defcan_cancel(self)->bool:returnselfisOrderStatus.PENDING@dataclass(frozen=True)classOrder:order_id:intquantity:intstatus:OrderStatusdef__post_init__(self)->None:ifself.quantity<=0: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._repository=repository self._released_quantity=0@propertydefreleased_quantity(self)->int:returnself._released_quantitydefadd(self,order:Order)->None:self._repository.save(order)defcancel(self,order_id:int)->Order:order=self._repository.find(order_id)iforderisNone:raiseOrderNotFoundError("订单不存在")ifnotorder.status.can_cancel():raiseValueError("当前状态不允许取消")cancelled=replace(order,status=OrderStatus.CANCELLED)self._repository.save(cancelled)self._released_quantity+=order.quantityreturncancelled

OrderRepository是本例可观察的存储边界,测试可以验证取消失败后订单没有被错误修改。正式项目更适合使用src目录布局并安装项目包;真实取消流程如果同时修改数据库订单和外部库存,还需要事务或幂等补偿,不能用这个内存示例推导生产一致性。

fixture:统一准备测试对象

创建tests/conftest.py:

importpytestfromorder_serviceimportOrderRepository,OrderService@pytest.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.py:

importpytestfromorder_serviceimport(Order,OrderNotFoundError,OrderRepository,OrderService,OrderStatus,)@pytest.mark.regression@pytest.mark.parametrize("quantity",[pytest.param(0,id="zero"),pytest.param(-1,id="negative"),],)deftest_order_quantity_must_be_positive(quantity:int)->None:withpytest.raises(ValueError,match="商品数量必须大于0"):Order(1000,quantity,OrderStatus.PENDING)@pytest.mark.smoke@pytest.mark.regressiondeftest_cancel_pending_order_releases_inventory(order_service:OrderService,order_repository:OrderRepository,)->None:order_service.add(Order(1001,2,OrderStatus.PENDING))actual=order_service.cancel(1001)assertactual.order_id==1001assertactual.statusisOrderStatus.CANCELLED stored=order_repository.find(1001)assertstoredisnotNoneassertstored.statusisOrderStatus.CANCELLEDassertorder_service.released_quantity==2@pytest.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)stored=order_repository.find(1002)assertstoredisnotNoneassertstored.statusisOrderStatus.PAIDassertorder_service.released_quantity==0@pytest.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)stored=order_repository.find(1003)assertstoredisnotNoneassertstored.statusisOrderStatus.CANCELLEDassertorder_service.released_quantity==2@pytest.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_quantity==0

pytest 会重写测试模块中的assert,失败时展示参与比较的值,不需要额外断言类。pytest.raises()会接受目标异常的子类,要求类型完全一致时可像不存在订单的用例一样断言type(exc_info.value)。match按正则表达式匹配异常文本,只应在消息属于接口或产品契约时使用;如果消息中包含[]、()等正则字符,可使用re.escape()。

正常用例同时检查返回对象、仓库状态和库存释放量,防止实现只返回新对象却漏掉持久化。异常用例则读取仓库确认状态未被修改,并检查released_quantity,防止失败路径错误释放库存。

参数化:覆盖状态规则

创建tests/test_order_status.py:

importpytestfromorder_serviceimportOrderStatus@pytest.mark.regression@pytest.mark.parametrize(("status","expected"),[pytest.param(OrderStatus.PENDING,True,id="pending"),pytest.param(OrderStatus.PAID,False,id="paid"),pytest.param(OrderStatus.CANCELLED,False,id="cancelled"),],)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-m"regression and not smoke"# 按名称表达式筛选python-mpytest-k"cancel and not missing"

-m匹配 mark,-k匹配测试节点名称、父级名称和关键字,两者可以组合使用,最终只执行同时满足两个表达式的测试。长期稳定的测试类型适合使用 mark,临时定位某个模块或函数时使用-k。

使用 pytest-xdist 并行执行

安装 pytest-xdist 后,通过-n指定 worker 数:

# 使用两个进程并行执行python-mpytest-n2# 根据可用 CPU 自动选择进程数python-mpytest-nauto# 同一模块的测试分配给同一 workerpython-mpytest-n2--distloadscope

pytest-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--durations=10--junitxml=reports/pytest.xml

--durations=10会列出最慢的 10 个 setup、call 或 teardown 阶段,适合判断慢点来自测试步骤还是 fixture。失败排查时可使用-x在首次失败后停止,或使用--maxfail=3限制失败数;修复后用--lf只执行上次失败的用例。不要在团队默认配置中长期启用--pdb,否则无人值守的 CI 会停在交互调试状态。

--lf依赖项目根目录下的.pytest_cache。虚拟环境、.pytest_cache、__pycache__和本地reports目录应加入.gitignore,CI 报告则通过流水线制品归档。

常见问题

问题处理方式
没有收集到测试检查文件、函数命名和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 并行。

相关新闻

  • 2026西宁黄金回收白银回收铂金回收工商备案可查全城上门回收旧金老店联系方式推荐
  • 【ModelArts】ECS部署后远程登陆失败:无法连接到远程主机,请检查主机网络与端口是否正确
  • 工业数据中心追求长效无间断稳定运行,一体化连通配电监测服务商有哪些?结合运行风险与扩容逻辑开展选型评估

最新新闻

  • 山东枣庄大跨距电缆桥架测评:悦尚电气质量优但价格略高,适用
  • 2026年杭州职业培训GEO代理服务商选型哪家靠谱?杭州GEO优化服务商代理加盟本地推荐指南 - 企业新闻快传
  • 广州番禺区搬家公司收费标准一览,涵盖各街道搬家报价,全天可拨打搬家服务联系号码汇总 - 厚道搬家
  • 中国公里网格人口分布数据集分享
  • 基于YOLOv8的多任务图像增强技术解析
  • 【Springboot毕设全套源码+文档】基于springboot林业资源管理系统设计与实现(丰富项目+远程调试+讲解+定制)

日新闻

  • 亨得利盐城维修点在哪里?手表维修保养地址指南**公示(2026年7月最新) - 亨得利官方
  • 提升.NET API安全性:Boxed.AspNetCore.Swagger认证授权最佳实践
  • 帝舵佛山**网点地址更新:2026年7月售后热线电话与服务客户指南 - 帝舵中国官方服务中心

周新闻

  • SaaS软件行业GEO实践:AI搜索时代的品牌可见性与获客新路径
  • 什么是PCTFE?医药高端包装的“防潮王牌“材料
  • 【JVM调优实战】16-可视化利器-JConsole-VisualVM-JMC

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号