ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

30分钟上手 pytest:从第一个断言到可维护的测试套件

30分钟上手 pytest:从第一个断言到可维护的测试套件 30分钟上手 pytest从第一个断言到可维护的测试套件【免费下载链接】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要解决的问题。它是 Python 生态里最主流的测试框架用一句大白话概括写小型测试只需一个assert但同样撑得起复杂的功能测试。这篇文章带你从零把 pytest 跑起来再跟一个真实小任务走一遍参数化、fixtures测试夹具即自动准备的测试资源和标记筛选最后打开黑盒看看它为什么这么省心。3 分钟拿到第一个结果一条命令完成安装只要你的环境里已经有 Python3.10 及以上执行pip install pytest装完立即验证--version能打印出版本号就算成功pytest --version # 输出类似pytest 9.x.x最小示例一个函数两条断言新建一个被测文件和一个测试文件。注意命名约定测试文件以test_开头测试函数同样以test_开头pytest 靠这个约定自动发现测试。# price.py def total(unit_price, count, discount0): return unit_price * count - discount# test_price.py from price import total def test_full_price(): assert total(30, 2) 60 def test_with_discount(): assert total(30, 2, discount10) 50在项目根目录执行pytest你会看到 test session starts collected 2 items test_price.py .. [100%] 2 passed in 0.01s 两个绿点就是两条通过的测试。把total(30, 2)故意改成减去 5 再跑一次失败信息会直接指出assert 55 60这样的细节——这是 pytest 断言重写的功劳后面专门讲。跟一个任务走给订单折扣写一套测试接下来用一个连贯的任务串起三个核心能力参数化、fixture 共享数据、marker 筛选。场景是电商订单模块的折扣函数金卡会员打 8 折、银卡 9 折、普通用户不打折# order.py def apply_discount(price, levelguest): ratio {gold: 0.8, silver: 0.9}.get(level, 1.0) return round(price * ratio, 2)第一步用参数化覆盖多种输入如果每种会员等级都写一个test_...函数代码会膨胀且难读。pytest.mark.parametrize让同一个测试函数跑多组输入每组在报告里单独计数# test_order.py import pytest from order import apply_discount pytest.mark.parametrize( (price, level, expected), [ (100, gold, 80.0), (99, silver, 89.1), (50, guest, 50), (0, gold, 0), ], ) def test_discount(price, level, expected): assert apply_discount(price, level) expected执行pytest -v4 组参数会展开成 4 条带编号的测试项任何一组挂掉都能精确定位到具体输入比如test_order.py::test_discount[100-gold-80.0] PASSED test_order.py::test_discount[99-silver-89.1] PASSED test_order.py::test_discount[50-guest-50] PASSED test_order.py::test_discount[0-gold-0] PASSED参数化的完整玩法含ids自定义用例名可参考官方文档 how-to/parametrize.rst。第二步用 fixture 准备共享的测试数据现在假设折扣规则来自一个配置文件多个测试都要读它。手写先建文件、再删文件的样板代码很烦而 fixture 把这个过程收进一个装饰器里——测试只需声明参数名pytest 就会自动注入pytest.fixture def rule_file(tmp_path): f tmp_path / rules.txt f.write_text(gold0.8\nsilver0.9\n) return f def test_rule_file_readable(rule_file): assert gold0.8 in rule_file.read_text() def test_gold_discount_from_file(rule_file): assert apply_discount(100, gold) 80.0这里用到了内置 fixturetmp_path它为每个测试提供一个独立的临时目录测试结束后自动清理你不用操心路径冲突。fixture 按名字声明、按作用域复用更多细节见 how-to/fixtures.rst 和核心实现 src/_pytest/fixtures.py。第三步用 marker 给测试打标、按条件运行有些用例需要拉真实数据源、跑得慢CI 里想默认跳过。给测试打上pytest.mark.slow标记再配合-m表达式就能筛选pytest.mark.slow def test_real_payment_gateway(): ... # 调用真实支付沙箱pytest # 全量运行 pytest -m not slow # 排除慢测试 pytest -m slow # 只跑慢测试提示自定义标记要在配置文件里登记见下一节否则 pytest 9.x 会在--strict-markers下对未声明的标记发出警告甚至报错这其实是帮你拼对了名字。打开黑盒它凭什么知道断言失败在哪断言重写把 assert 变成带调试信息的断言原生 Python 的assert a b失败时只会说AssertionError不说a和b各是多少。pytest 在收集阶段用 AST抽象语法树分析测试文件把每条assert改写成等价的、记录中间变量值的代码再注入执行。所以你看到的失败信息是这样的def test_discount(): price, level 100, gold actual apply_discount(price, level) expected 85.0 assert actual expected E assert 80.0 85.0嵌套结构列表、字典的逐元素对比、pytest.approx的浮点近似比较也都建立在这套机制上。这套逻辑集中在 src/_pytest/assertion/ 目录入口是 rewrite.py。注意断言重写只作用于测试文件本身普通python -O优化模式或某些打包方式下重写可能被禁用调试失败信息缺失时可先怀疑这里。fixture 的解析顺序依赖图 作用域fixture 不是执行顺序而是一张依赖图每个测试声明了哪些 fixture 参数pytest 就从最外层作用域往内逐层实例化用完再按相反顺序回收。作用域从短到长是function → class → module → session决定了资源何时重建作用域重建时机典型用途function每个测试前后临时文件、mockmodule每个测试文件一次读配置文件、建连接池session整个运行过程一次数据库连接、浏览器实例运行调度本身由 src/_pytest/runner.py 驱动负责 setup、call、teardown 三段式执行并收集结果。按需定制配置、开关与扩展把重复参数写进 pytest.ini在pytest目录下放一个pytest.ini或pyproject.toml的[tool.pytest.ini_options]常用参数就不用每次手敲[pytest] testpaths tests addopts -ra --strict-markers markers slow: 慢速测试用 -m not slow 排除testpaths默认收集目录直接pytest就只跑tests/addopts -ra结尾汇总列出所有 skip/xfail/failures命令行的优先级高于配置文件临时调试随时可覆盖命令行速查表命令作用什么时候用pytest -v逐条列出测试名定位失败用例pytest -x首败即停快速修 bugpytest --lf只重跑上次失败项改完代码后二次验证pytest -k discount按名称子串过滤不想改文件只跑相关测试pytest --pdb失败时进入调试器交互式排查pytest --junitxmlreport.xml生成 JUnit XML喂给 CI 平台pytest --collect-only只收集不执行检查发现逻辑对不对更多用法见 how-to/usage.rst完整配置项索引在 reference/index.rst。写一个最小插件pytest 的扩展点是钩子函数在conftest.py里定义特定签名的函数即可介入流程。比如下面这个把折扣相关测试自动打上slow标记# conftest.py def pytest_collection_modifyitems(items): for item in items: if test_order in item.nodeid: item.add_marker(slow)想深入插件体系含第三方插件开发看 how-to/writing_plugins.rst退出码含义0 全过、1 有失败、2 中断……定义在 reference/exit-codes.rstCI 里判断结果就靠它。避坑与提速新手最常踩的三个坑命名不达标导致0 条测试文件没叫test_*.py、函数没叫test_*收集结果会是空。先跑pytest --collect-only看它发现了什么比猜快得多。fixture 作用域开太大给共享数据库连接图省事用了scopesession结果测试之间互相污染、单独跑过一起跑挂。原则是能小就小隔离优先。断言信息突然变糙用python -O启动或某些打包场景会跳过断言重写失败输出只剩裸AssertionError。对照打开黑盒一节检查运行方式。让套件跑得更快修 bug 时组合pytest -x --lf只重跑失败项且首败即停单轮反馈从分钟级压到秒级大套件可引入第三方插件 pytest-xdist 做并行-n auto但注意 fixture 作用域与并行隔离的交互项目里自带了 bench/ 基准目录和海量自测用例 testing/想知道某个写法在大规模收集下性能如何可以直接参考这些脚本的写法收口值不值得投入你刚才已经验证过了回到开头的痛点现在你有一行pytest命令就能在几秒内回答这行改动会不会打破别的。下一步你可以按需深入——用pytest.mark.parametrize给现有函数补齐边界用例、把散落的测试数据迁进 fixture、再用pytest.ini固化团队约定想造轮子就从给conftest.py加一个钩子开始。完整文档在 doc/en/ 下祝测试全绿。【免费下载链接】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),仅供参考
返回列表