ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

高级测试工程师课程(第11章):PyTest 框架实战——fixture、参数化、mock、HTML报告与hook机制全打通

高级测试工程师课程(第11章):PyTest 框架实战——fixture、参数化、mock、HTML报告与hook机制全打通 高级测试工程师课程第11章PyTest 框架实战——fixture、参数化、mock、HTML报告与hook机制全打通前言pytest 是 Python 测试领域事实上的标准框架。相比标准库 unittest它用更少的代码表达更强的能力自动发现、fixture 依赖注入、参数化数据驱动、丰富的插件生态。本章我在 Ubuntu 24.04 服务器上基于pytest 9.1.1完整实操了课程第 11 章内容unittest 与 pytest 正面对比、pytest.ini 配置、-k/-m执行控制、fixture 四种 scope 的生命周期实证、conftest.py 共享、pytest.mark.parametrize 数据驱动、unittest.mock 打桩、pytest-html 报告、conftest hook 机制全部真实执行并附真实输出。一、实验环境项目配置云主机华为云 ECS8 vCPU / 14GB 内存操作系统Ubuntu 24.04.4 LTS (noble)Python3.12.3虚拟环境 /root/venvpytest9.1.1pytest-html4.2.0pluggy 1.6.0requests2.34.2mock 演示用实操目录/root/pytest-lab安装过程$aptinstall-ypython3-venvpython3-mvenv /root/venv $ /root/venv/bin/pip configsetglobal.index-url https://repo.huaweicloud.com/repository/pypi/simple $ /root/venv/bin/pipinstallpytest pytest-html requests $ /root/venv/bin/pytest--versionpytest9.1.1二、为什么用 pytest与 unittest 正面对比2.1 被测函数先把被测代码myfunc.py摆出来——一个字符串转整数并校验的函数后续所有用例都围绕它defstr_to_int(s):字符串转整数去首尾空白、支持正负号、非法输入抛异常ifnotisinstance(s,str):raiseTypeError(f期望str, 实际{type(s).__name__})ss.strip()ifnots:raiseValueError(空字符串)bodys.lstrip(-)ifnotbody.isdigit():raiseValueError(f{s!r}不是合法整数)returnint(s)2.2 unittest 版本importunittestclassTestStrToInt(unittest.TestCase):defsetUp(self):print(\n[unittest] setUp 每条用例前执行)deftearDown(self):print([unittest] tearDown 每条用例后执行)deftest_normal(self):self.assertEqual(str_to_int(123),123)deftest_invalid_raises(self):withself.assertRaises(ValueError):str_to_int(abc)# ... 共5条2.3 pytest 版本importpytestpytest.fixture(autouseTrue)defaround_each():print(\n[pytest] 前置)yieldprint([pytest] 后置)deftest_normal():assertstr_to_int(123)123deftest_invalid_raises():withpytest.raises(ValueError):str_to_int(abc)2.4 真实运行对比unittest 运行$ /root/venv/bin/python -m unittest discover -s tests -p test_unittest* -v test_invalid_raises (test_unittest_style.TestStrToInt.test_invalid_raises) ... ok test_negative (test_unittest_style.TestStrToInt.test_negative) ... ok test_normal (test_unittest_style.TestStrToInt.test_normal) ... ok test_strip_space (test_unittest_style.TestStrToInt.test_strip_space) ... ok test_type_error (test_unittest_style.TestStrToInt.test_type_error) ... ok ---------------------------------------------------------------------- Ran 5 tests in 0.000s OKpytest 运行同功能用例全量 32 条摘取关键行$ /root/venv/bin/pytest -v test session starts platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0 -- /root/venv/bin/python3 rootdir: /root/pytest-lab configfile: pytest.ini testpaths: tests plugins: html-4.2.0, metadata-3.1.1 collected 32 items tests/test_pytest_style.py::test_invalid_raises PASSED [ 28%] tests/test_pytest_style.py::test_negative PASSED [ 37%] tests/test_pytest_style.py::test_normal PASSED [ 43%] ... 32 passed in 0.18s 2.5 对比结论维度unittestpytest用例写法必须继承 TestCase类方法普通函数即可 ✅断言self.assertEqual/assertRaises 等十几种原生 assert失败自动展开对比 ✅前后置setUp/tearDown 固定命名fixture yield可组合可复用 ✅参数化需 subTest 或第三方 ddtpytest.mark.parametrize 内置 ✅插件生态基本没有1600 插件html/allure/xdist…✅有意思的是pytest 能直接运行 unittest 风格的用例上面 32 条里就包含了 unittest 类的 5 条迁移成本几乎为零。再补充一个断言层面的细节pytest 对原生assert做了断言内省assertion introspection——当assert a b失败时pytest 会自动展开两边的值做 diff 展示告诉你左边是什么、右边是什么、差在哪个字符。而 unittest 如果偷懒用self.assertTrue(a b)失败时只会告诉你False is not true排障体验天差地别。这也是 pytest 敢让你直接用裸 assert 的底气它重写了 assert 语句的字节码在不增加任何语法负担的前提下提供了比专用断言方法更强的诊断信息。三、测试发现与 pytest.ini 配置3.1 配置文件[pytest] testpaths tests addopts -ra markers smoke: 冒烟用例核心链路 slow: 慢速用例耗时长日常跳过 disable_test_id_escaping_and_forfeit_all_rights_to_community_support Truetestpaths默认只从 tests 目录收集避免扫到虚拟环境。addopts -ra每次运行自动附加-r选项末尾汇总显示跳过/失败原因。markers注册自定义标记不注册会有 warning。最后一行解决中文 ids 转义问题详见踩坑记录。3.2 -k 按名称过滤真实输出$ /root/venv/bin/pytest -v tests/test_parametrize.py -k str_to_int collected 17 items / 6 deselected / 11 selected tests/test_parametrize.py::test_str_to_int_fail[None类型] PASSED [ 9%] tests/test_parametrize.py::test_str_to_int_fail[字母] PASSED [ 18%] tests/test_parametrize.py::test_str_to_int_fail[小数] PASSED [ 27%] tests/test_parametrize.py::test_str_to_int_fail[空串] PASSED [ 36%] tests/test_parametrize.py::test_str_to_int_ok[前导零] PASSED [ 45%] tests/test_parametrize.py::test_str_to_int_ok[大数] PASSED [ 54%] tests/test_parametrize.py::test_str_to_int_ok[带空格] PASSED [ 63%] tests/test_parametrize.py::test_str_to_int_ok[显式正号] PASSED [ 72%] tests/test_parametrize.py::test_str_to_int_ok[普通] PASSED [ 81%] tests/test_parametrize.py::test_str_to_int_ok[负数] PASSED [ 90%] tests/test_parametrize.py::test_str_to_int_ok[零] PASSED [100%]-k支持表达式-k login and not slow按用例名关键字灵活筛选。3.3 -m 按标记过滤真实输出$ /root/venv/bin/pytest -v -m smoke collected 32 items / 31 deselected / 1 selected tests/test_parametrize.py::test_login_smoke PASSED [100%] 1 passed, 31 deselected in 0.06s $ /root/venv/bin/pytest -v -m not slow 31 passed, 1 deselected in 0.07s 解读-m smoke精准命中 1 条冒烟用例-m not slow反向排除 1 条慢速用例。CI 流水线里PR 阶段跑 smoke、夜间跑全量就是这么实现的 ✅。四、fixture 与 scope四种作用域生命周期实证4.1 setup/teardown vs fixtureunittest 的 setUp/tearDown 是一刀切每条用例前后都执行无法表达整个会话只登录一次。pytest 的 fixture 用scope参数解决这个分层问题。4.2 演示代码importpytestpytest.fixture(scopesession)deff_session():print(\n [session] 整个测试会话只执行1次)yieldsessionprint(\n [session] 会话结束销毁)pytest.fixture(scopemodule)deff_module():...pytest.fixture(scopeclass)deff_class():...pytest.fixture(scopefunction)deff_function():...classTestGroupA:deftest_a1(self,f_session,f_module,f_class,f_function):print( 执行 test_a1)deftest_a2(self,f_session,f_module,f_class,f_function):print( 执行 test_a2)classTestGroupB:deftest_b1(self,f_session,f_module,f_class,f_function):print( 执行 test_b1)4.3 真实运行输出pytest -s$ /root/venv/bin/pytest -v -s tests/test_fixture_scope.py collected 3 items tests/test_fixture_scope.py::TestGroupA::test_a1 [session] 整个测试会话只执行1次 [module] 每个模块执行1次 [class] 每个类执行1次 [function] 每条用例执行1次 执行 test_a1 PASSED [function] 用例结束销毁 tests/test_fixture_scope.py::TestGroupA::test_a2 [function] 每条用例执行1次 执行 test_a2 PASSED [function] 用例结束销毁 [class] 类结束销毁 tests/test_fixture_scope.py::TestGroupB::test_b1 [class] 每个类执行1次 [function] 每条用例执行1次 执行 test_b1 PASSED [function] 用例结束销毁 [class] 类结束销毁 [module] 模块结束销毁 [session] 会话结束销毁 3 passed in 0.01s 4.4 解读输出是教科书级的证据链session3 条用例全程只在 test_a1 前创建 1 次全部用例跑完最后才销毁 ✅classTestGroupA 结束时销毁TestGroupB 开始时重新创建 ✅function每条用例前创建、用后销毁共 3 次 ✅销毁顺序与创建顺序严格相反栈式module 在最后一个 class 销毁后才销毁。实战建议数据库连接/token 用 session scope测试数据准备用 class/module scope每条用例的临时状态用默认 function scope。4.5 fixture 的两个进阶知识点1autouse 自动生效。给 fixture 加autouseTrue后作用域内所有用例不需要在参数列表里声明也会自动执行适合每条用例都要做的隐性准备工作比如清理临时目录、记录用例开始时间。第二节 pytest 版本对比代码里的around_each就是 autouse fixture5 条用例没有一条显式引用它但每条前后都打印了前置/后置。要节制使用——autouse 过多会让用例的实际依赖变得隐晦新人看用例时不知道背后还跑了什么。2fixture 之间的依赖与参数化组合。fixture 可以像用例一样在参数里声明引用其他 fixturepytest 会按依赖图自底向上组装。当多个带 params 的 fixture 被同一条用例引用时会产生笛卡尔积组合比如 browser3 个值× env2 个值 6 条用例。这是做多维度兼容性测试的利器但也要警惕组合爆炸3 个各带 5 个参数的 fixture 就是 125 条用例。4.6 常用命令行参数速查参数作用实战场景-v显示每条用例名日常调试-s不捕获 print 输出观察 fixture 打印见4.3节-k 表达式按名称筛选只跑某个模块的用例-m 标记按标记筛选CI 分层执行 smoke/全量-x第一个失败立即停止快速反馈联调阶段–lf只跑上次失败的修复后回归验证-n auto多进程并行pytest-xdist千级用例提速–html生成 HTML 报告pytest-html结果归档五、conftest.py 共享 fixture 与 fixture 参数化5.1 conftest.pyimportpytestpytest.fixturedefbase_url():共享fixture所有用例可直接使用returnhttps://api.example.compytest.fixture(params[chrome,firefox,edge])defbrowser(request):fixture参数化一个用例自动跑3遍returnrequest.paramconftest.py 放在哪个目录其 fixture 就对哪个目录含子目录生效不需要 importpytest 自动注入。5.2 使用与真实运行deftest_fixture_param_browser(browser):print(f\n 当前浏览器:{browser})assertbrowserin(chrome,firefox,edge)deftest_shared_base_url(base_url):assertbase_url.startswith(https://)tests/test_parametrize.py::test_fixture_param_browser[chrome] PASSED [ 18%] tests/test_parametrize.py::test_fixture_param_browser[edge] PASSED [ 21%] tests/test_parametrize.py::test_fixture_param_browser[firefox] PASSED [ 25%] tests/test_parametrize.py::test_shared_base_url PASSED [ 50%]解读fixture 加params后引用它的用例被自动展开成 3 条chrome/edge/firefox 各一遍这就是 fixture 级参数化常用于多浏览器、多环境dev/test/staging切换 ✅。六、pytest.mark.parametrize 数据驱动6.1 代码11 组参数化用例含异常用例pytest.mark.parametrize(raw,expected,[(123,123),( 42 ,42),(-7,-7),(0,0),(8,8),(007,7),(999999999999,999999999999),],ids[普通,带空格,负数,零,显式正号,前导零,大数])deftest_str_to_int_ok(raw,expected):assertstr_to_int(raw)expectedpytest.mark.parametrize(raw,exc,[(abc,ValueError),(,ValueError),(12.3,ValueError),(None,TypeError),],ids[字母,空串,小数,None类型])deftest_str_to_int_fail(raw,exc):withpytest.raises(exc):str_to_int(raw)6.2 真实运行输出tests/test_parametrize.py::test_str_to_int_ok[普通] PASSED [ 81%] tests/test_parametrize.py::test_str_to_int_ok[带空格] PASSED [ 63%] tests/test_parametrize.py::test_str_to_int_ok[负数] PASSED [ 90%] tests/test_parametrize.py::test_str_to_int_ok[零] PASSED [100%] tests/test_parametrize.py::test_str_to_int_ok[显式正号] PASSED [ 78%] tests/test_parametrize.py::test_str_to_int_ok[前导零] PASSED [ 68%] tests/test_parametrize.py::test_str_to_int_ok[大数] PASSED [ 54%] tests/test_parametrize.py::test_str_to_int_fail[字母] PASSED [ 18%] tests/test_parametrize.py::test_str_to_int_fail[空串] PASSED [ 36%] tests/test_parametrize.py::test_str_to_int_fail[小数] PASSED [ 27%] tests/test_parametrize.py::test_str_to_int_fail[None类型] PASSED [ 9%]解读两个函数各 7 组、4 组数据共 11 条用例全过 ✅。新增一组数据只需在列表里加一行真正的数据驱动。ids参数给每组数据起中文名报告里可读性拉满。异常用例用pytest.raises(exc)断言必须抛出指定异常(None, TypeError)这组验证了类型校验分支。七、unittest.mock 打桩外部依赖7.1 被测代码importrequestsdeffetch_username(base_url,user_id):调用外部接口获取用户名resprequests.get(f{base_url}/users/{user_id},timeout5)resp.raise_for_status()returnresp.json()[name]7.2 mock 用例fromunittest.mockimportMock,patchdeftest_fetch_username_mock():fake_respMock()fake_resp.json.return_value{id:1,name:张三}fake_resp.raise_for_status.return_valueNonewithpatch(myfunc.requests.get,return_valuefake_resp)asm:namefetch_username(https://api.example.com,1)assertname张三m.assert_called_once_with(https://api.example.com/users/1,timeout5)deftest_fetch_username_http_error():fake_respMock()fake_resp.raise_for_status.side_effectException(404 Not Found)withpatch(myfunc.requests.get,return_valuefake_resp):withpytest.raises(Exception,match404):fetch_username(https://api.example.com,999)7.3 真实运行输出tests/test_mock_demo.py::test_fetch_username_http_error PASSED [ 12%] tests/test_mock_demo.py::test_fetch_username_mock PASSED [ 15%]解读patch(myfunc.requests.get)的关键是打桩位置要在被测模块的命名空间myfunc 里 import 的 requests而不是requests.get本身——这是 mock 最常见的错误 ❌。side_effect模拟异常分支assert_called_once_with还能反向验证被测代码发起的请求参数是否正确。全程零网络请求用例跑得又快又稳 ✅。八、pytest-html 测试报告8.1 生成命令与真实结果$ /root/venv/bin/pytest --htmlreport.html --self-contained-html ---------- Generated html report: file:///root/pytest-lab/report.html ---------- 32 passed in 0.18s $ ls -l report.html -rw-r--r-- 1 root root 56599 Sep 5 17:11 report.html--self-contained-html把 CSS/JS 全部内联进单个 HTML 文件约 55KB方便邮件发送和归档。从报告内嵌 JSON 中提取到的统计信息$ python3 -c ...(解析report.html内嵌jsonblob)... 报告环境: {Python: 3.12.3, Platform: Linux-6.8.0-106-generic-x86_64-with-glibc2.39, Packages: {pytest: 9.1.1, pluggy: 1.6.0}, Plugins: {html: 4.2.0, metadata: 3.1.1}} 用例总数: 32报告包含环境信息Python/平台/插件版本、32 条用例的通过状态、每条用例的执行耗时、失败用例的完整 traceback 与捕获的 print 输出。对更美观的报告可换 allure-pytest但 pytest-html 胜在零依赖单文件 ✅。九、hook 机制pytest_collection_modifyitems9.1 conftest.py 实现defpytest_collection_modifyitems(items):hook演示1给名字含 login 的用例自动加 smoke 标记 hook演示2用例按名称排序重排序foriteminitems:iflogininitem.name:item.add_marker(pytest.mark.smoke)items.sort(keylambdai:i.name)9.2 效果验证回看第三节-m smoke的真实输出全量 32 条里只有 1 条 smoke 被选中说明两个 hook 都生效了用例执行顺序从文件顺序变成了按用例名排序对照全量输出PASSED 顺序按字母排列✅实际项目中这个 hook 常用于按标记自动分流、动态 skip、给超时用例自动加pytest.mark.timeout。hook 机制是 pytest 插件体系的根基pluggy插件作者写的pytest_xxx函数和我们写在 conftest.py 里的没有本质区别。9.3 常用 hook 速查表hook 函数触发时机典型用途pytest_collection_modifyitems用例收集完成后重排序、自动加标记、动态 skippytest_runtest_setup每条用例执行前按标记检查环境、前置拦截pytest_runtest_makereport每条用例出结果时失败自动截图、写失败日志pytest_sessionfinish整个会话结束后推送结果到测试平台、发通知pytest_addoption解析命令行参数时给 pytest 增加自定义命令行选项以失败自动截图为例在 UI 自动化项目里实现pytest_runtest_makereport当 report.failed 时调用浏览器的截图 API 并把图片路径挂到报告里这是几乎所有 pytest 系 UI 框架如 pytest-selenium 生态的标配做法。hook 的本质是 pytest 在生命周期的关键节点上预留的回调conftest.py 里同名函数会被 pluggy 自动发现并按序调用多个插件实现同一 hook 时按注册顺序形成调用链。十、插件生态简介pytest 真正的护城河是插件生态目前 PyPI 上以 pytest- 开头的插件超过 1600 个。测试工程师最该知道的几个插件作用一句话评价pytest-html生成单文件 HTML 报告轻量零依赖本章已实操 ✅allure-pytest生成 Allure 美观报告颜值高需装 Allure 命令行企业项目首选pytest-xdist多进程并行执行千级用例提速利器pytest -n autopytest-rerunfailures失败自动重跑对付偶发 flaky 用例但会掩盖真问题慎用pytest-timeout用例超时强制中断防止某个用例卡死拖垮整个流水线pytest-ordering控制用例执行顺序有依赖的用例才用能用 hook 就别装插件pytest-assume软断言失败后继续后续断言一条用例验证多个字段时很有用选型建议先吃透内置能力fixture/参数化/marker/hook再按需引入插件。插件装多了执行变慢、版本冲突概率上升而且很多插件功能用 conftest.py 十几行代码就能实现——比如本章第九节的用例重排序和自动加标记就不需要 pytest-ordering。十一、踩坑记录坑1中文 parametrize ids 被转义本次实操真实遇到tests/test_parametrize.py::test_str_to_int_fail[None类型] - 修复前显示 None\u7c7b\u578b tests/test_parametrize.py::test_str_to_int_fail[\u5b57\u6bcd] PASSED解决pytest.ini 加一行配置即可上面 -k 输出已是修复后的效果disable_test_id_escaping_and_forfeit_all_rights_to_community_support True这个配置项名字又臭又长社区自嘲但确实有效 ✅。坑2patch 打桩位置错误patch(requests.get)不会生效必须patch(myfunc.requests.get)——打桩打到被测模块引用它的位置。坑3Ubuntu 24.04 系统 pip 拒绝安装externally-managed-environment 错误✅ 正确姿势是 venv 虚拟环境❌ 不要用--break-system-packages污染系统 Python。十二、总结知识点实操结果unittest vs pytest 对比5 条同功能用例双框架各跑一遍全过 ✅pytest.ini -k/-m32 条用例-m smoke 命中 1 条-k 命中 11 条 ✅fixture 四种 scope打印顺序完整证明生命周期 ✅conftest 共享 fixture 参数化browser 参数自动展开 3 条用例 ✅parametrize 数据驱动11 组用例含 4 组异常断言全过 ✅unittest.mock正常异常两分支 mock零网络 ✅pytest-html 报告单文件 55KB 报告32 passed ✅hook 机制自动加标记用例重排序 ✅全量执行结果32 passed in 0.18s代码留存在/root/pytest-lab。参考链接pytest 官方文档https://docs.pytest.org/pytest-html 插件https://pypi.org/project/pytest-html/unittest.mock 文档https://docs.python.org/zh-cn/3.12/library/unittest.mock.html
返回列表