10分钟跑通你的第一个pytest测试:Python测试框架手把手教程
【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest
改完一行折扣逻辑提交后,用户反馈账单金额不对。你手动在控制台里重跑几个输入找原因,却容易漏掉别的分支。pytest测试框架解决的就是这个问题:把你关心的"输入 → 期望输出"写成一个个函数,改完代码后一条命令全部重跑,并精确告诉你哪一行错了。本教程会帮你用10分钟跑通第一个测试,再学会断言、夹具、参数化三招,最后为项目配上pytest.ini,让它长期稳定运行。
🏁 跑通第一个测试:装好pytest并读懂运行输出
确认 Python 3.10 以上后,一条命令装好 pytest 测试框架:
pip install pytest写一个贴近真实业务的最小用例。这里用一个折扣函数代替玩具例子,测试文件和被测函数可以放在同一个文件里快速验证:
# test_pricing.py def discount(price: float, coupon: float) -> float: return max(price - coupon, 0.0) # 优惠券不能把金额扣成负数 def test_coupon_deducts_from_price(): assert discount(100.0, 20.0) == 80.0 # 正常抵扣 def test_coupon_cannot_go_negative(): assert discount(15.0, 20.0) == 0.0 # 金额下限为 0在项目根目录敲pytest -v(-v表示逐条列出每个用例的结果),你会看到:
=========================== test session starts ============================ platform linux -- Python 3.12, pytest-9.0.x, pluggy-1.6 -- /home/you/.venv/bin/python cachedir: .pytest_cache rootdir: /home/you/checkout collecting ... collected 2 items tests/test_pricing.py::test_coupon_deducts_from_price PASSED [ 50%] tests/test_pricing.py::test_coupon_cannot_go_negative PASSED [100%] ============================== 2 passed in 0.03s ===============================前三行是环境信息:platform显示 Python 和 pytest 版本,rootdir是 pytest 定位到的项目根目录(配置文件所在处)。collected 2 items表示它按文件名和函数名收集到了 2 个用例——文件要叫test_*.py,函数要叫test_*,这是收集规则。最后一行的passed in 0.03s是总结论:全部通过,耗时 0.03 秒。
🧩 看懂pytest内部结构:一张表讲清6个核心模块
pytest 的所有核心代码都在src/_pytest/下。一次运行大致按这个顺序流转:
pytest(命令行入口) └─ config 解析参数、加载 pytest.ini、定位 rootdir └─ collect 扫描 test_*.py,收集出一个个用例 └─ runner 逐个执行 ├─ fixtures 用例运行前准备、运行后清理 ├─ assertion 改写 assert,失败时给出详细对比 └─ terminal 把结果渲染成上面的终端报告对应到真实目录,主要模块如下(路径均相对仓库根目录):
| 模块 | 职责 | 真实路径 |
|---|---|---|
| config | 解析命令行参数、加载配置、定位项目根 | src/_pytest/config/ |
| fixtures | 夹具系统:依赖注入与生命周期管理 | src/_pytest/fixtures.py |
| assertion | 断言重写,失败时生成可读的对比信息 | src/_pytest/assertion/ |
| mark | @pytest.mark标记与按标记筛选 | src/_pytest/mark/ |
| runner | 执行每个测试项并收集结果 | src/_pytest/runner.py |
| terminal | 渲染.、PASSED等终端报告 | src/_pytest/terminal.py |
你平时只和命令行交互,但这张表解释了"为什么-m筛选生效""为什么失败信息这么详细"——分别对应 mark 和 assertion 两个模块。
✍️ 写好用例的三个进阶招式:断言、夹具、参数化
招式一:靠断言重写读懂失败现场
一句话价值:pytest 会在收集阶段改写你的assert,失败时自动打印两边实际值,不用你手写打印。
把折扣函数的max删掉模拟一个 bug,重新运行:
tests/test_pricing.py:9: in test_coupon_cannot_go_negative assert discount(15.0, 20.0) == 0.0 E assert -5.0 == 0.0E行直接告诉你:函数返回了-5.0而不是期望的0.0,定位不到问题就不用猜。涉及浮点数时改用近似比较:
from pytest import approx assert 0.1 + 0.2 == approx(0.3) # 浮点误差范围内视为相等什么时候该用:所有断言场景。普通==适合整数、字符串、结构体;浮点运算、金额计算一律approx。改写机制的实现在 src/_pytest/assertion/。
招式二:用夹具替代手写setup/teardown
一句话价值:夹具(fixture)让你把"准备测试数据"的代码从每个用例里抽出来,按函数名注入。
import pytest @pytest.fixture def invoice(): # 夹具名就是测试函数的参数名 return {"items": ["keyboard", "mouse"], "amount": 499.0} def test_invoice_amount_positive(invoice): # 参数名对上即自动注入 assert invoice["amount"] > 0夹具还支持作用域:@pytest.fixture(scope="session")表示整个会话只初始化一次,适合数据库连接这类昂贵的资源。
什么时候该用:同一个用例要准备超过 3 行数据,或者多个用例共享同一份数据时。实现入口在 src/_pytest/fixtures.py。
招式三:用参数化消灭复制粘贴
一句话价值:同一个断言逻辑要验证多组输入时,用@pytest.mark.parametrize写一次、跑 N 遍。
import pytest @pytest.mark.parametrize( ("price", "coupon"), [(100.0, 20.0), (15.0, 20.0), (9.9, 5.0)], # 每组数据独立跑一遍下面的函数 ) def test_discount_never_negative(price, coupon): assert discount(price, coupon) >= 0.0运行后报告会列出 3 条用例,哪组数据挂了一目了然。
什么时候该用:边界值测试(0、负数、上限)、多版本兼容、不同配置的对比。只要发现自己在复制测试函数只改数字,就该参数化。
🔧 让测试跑稳:pytest.ini配置与报告命令
用pytest.ini固定项目默认行为
在项目根目录建pytest.ini,团队所有人跑pytest时自动带上这些设定:
[pytest] addopts = -v --tb=short # 默认逐条显示,失败信息用短格式 testpaths = tests # 不传路径时默认扫 tests 目录 pythonpath = src # 把 src 加入导入路径 markers = slow: 慢速用例,用 -m "not slow" 跳过配合目录组织,项目保持这种结构即可:
myproject/ ├── src/myproject/ # 源码包 ├── tests/ # 测试,文件名 test_*.py │ └── test_pricing.py └── pytest.ini用命令行控制每次怎么跑
pytest -m "not slow" # 只跳过标记为 slow 的用例,其余照跑 pytest -x # 第一个失败就停下,适合逐条修复 pytest --lf # 只重跑上次失败的那些(依赖 .pytest_cache) pytest --junitxml=report.xml # 输出 XML 报告给 CI 平台消费--lf依赖缓存模块 src/_pytest/cacheprovider.py,第一次跑完会在.pytest_cache/里记录失败清单,日常迭代用它最省时间。
⚠️ 新手最容易踩的几个坑:5个高频问题
Q:为什么报collected 0 items,一个测试都没跑?收集规则是死约定:文件必须以test_开头、函数必须以test_开头,且不能带多余参数(test_foo(self)这种 unittest 写法在这里无效)。改对命名后先跑pytest --collect-only确认收集到了再执行。
Q:运行报ModuleNotFoundError,但代码里 import 明明没问题?pytest 的 rootdir 和导入路径和你在 IDE 里点运行不是一回事。把源码放在src/下时,在pytest.ini里加一行pythonpath = src即可;或者用pip install -e .把项目装进当前环境。
Q:两个目录里各有一个同名test_helper.py,运行直接报错?这是导入冲突:非包目录里的同名文件会撞车。给目录加__init__.py组成包,或者把测试文件重命名成全局唯一。详细的导入机制见 doc/en/explanation/pythonpath.rst。
Q:夹具明明写了,用例却提示找不到对应参数?注入靠参数名精确匹配:@pytest.fixture的函数名必须和用例参数名完全一致。另外夹具定义在别的测试文件里时不共享,公共夹具要放到tests/conftest.py中。
Q:改了代码重跑太慢,怎么只跑刚才失败的那批?pytest --lf(last-failed)。注意它只认缓存里的记录:一次全部通过后缓存清空,--lf会退化成全量运行,这属于正常行为。
pytest 用"文件即用例、断言即检查、夹具即环境"三件事,覆盖了大部分日常测试需求。下一步可以:看 doc/en/how-to/writing_plugins.rst 写自己的插件、研究testing/plugins_integration/里与 Django 等框架的集成方式、或用pytest --benchmark类插件给热点函数做性能基准。
【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考