ARTICLE DETAIL

资讯详情

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

pytest从入门到实战:fixture、参数化与Allure报告全攻略

pytest从入门到实战:fixture、参数化与Allure报告全攻略 开头先讲一个我自己的真实经历。早几年我在一个后端项目里维护测试用的还是unittest风格SetUp/TearDown包了一层又一层断言必须写self.assertEqual、self.assertTrue换一个字段就要去查API文档用例一多整个测试文件跟裹脚布似的。后来团队引入pytest框架同样的场景代码量直接砍了一半定位失败还更清楚。从那以后我再也没写过一行业余风格的测试用例。这篇内容就是基于我实际用pytest跑项目的经验整理的围绕pytest的安装方式、pytest在PyCharm里的配置、pytest和allure报告的组合用法完整走一遍入门到上手的过程。1. 为什么我把测试工作流从unittest切到了pytest很多刚接触pytest的人第一个问题都是Python自带unittest为什么还要折腾一个第三方框架这个问题我当时也问过现在回头看答案其实很朴实——pytest解决的是写测试和读测试这两件事的体验问题。1.1 unittest的痛点样板代码和API记忆负担我最早用unittest写接口测试时最常见的代码长这样import unittest class TestMath(unittest.TestCase): def setUp(self): # 每次用例执行前的准备工作 self.base 10 def tearDown(self): # 收尾工作 pass def test_add(self): result self.base 5 self.assertEqual(result, 15) def test_sub(self): result self.base - 3 self.assertEqual(result, 7)对照一下pytest的写法def test_add(): base 10 assert base 5 15 def test_sub(): base 10 assert base - 3 7差别很明显。unittest要求你背一堆断言方法从assertEqual到assertAlmostEqual、assertRaises、assertIn、assertIsInstance每一种都有固定名字。pytest直接复用Python原生的assert关键字条件不满足就抛异常失败信息里直接看到对比值。没有继承关系、没有setUp/tearDown约束测试函数就是普通函数。1.2 pytest的优雅到底体现在哪些层面说白了优雅不是玄学是可感知的效率差。第一是收集规则简单。默认递归查找当前目录下所有test_.py或_test.py文件文件里的test_函数、Test类下的test_方法都会自动被收集。不需要像unittest那样手动声明suite和runner也不用把所有用例组装成一个列表再交给main执行。第二是fixture体系灵活。unittest的setUp/tearDown只能按类和模块组织不同用例需要不同前置条件时你得拆类、拆继承。pytest的fixture可以精确到函数级别可以自由组合、可复用、可控制执行范围后面我单独展开讲。第三是插件生态。从生成报告、失败重试、并行执行到参数化数据驱动几乎都能找到现成的插件。unittest虽然也能做但很多能力要么自己手搓要么就是没有统一标准。1.3 一个常见的怀疑pytest是不是只适合小脚本我遇到过有人担心pytest用起来太随意项目一大了会不会乱套。这个担心可以理解但实际情况是pytest的约束力来自于约定加插件。你可以在项目根目录放conftest.py统一管理fixture可以用marker给用例打标签分类可以用pytest.ini或pyproject.toml固定配置项。它看起来自由但自由之上完全可以建立秩序。下面这张表是两种框架的直观对比适合你在团队内部做方案评估时直接用对比维度unittestpytest用例识别必须继承TestCase类按命名规则自动收集无需继承断言写法self.assertXxx系列API原生assert表达式前置/后置setUp/tearDown体系fixture依赖注入可精确控制参数化无内置需要组合subTest内置pytest.mark.parametrize失败信息相对简略需自行格式化自动展示断言上下文和变量值插件生态少且分散丰富覆盖报告、重试、并行等场景自定义扩展需要写大量样板基于hook函数轻量2. Pytest的安装、PyCharm配置与第一个用例跑通任何框架的入门都卡在环境上。pytest的安装过程本身不难但有几个细节新手容易栽跟头比如pip版本太旧、装了多个Python解释器导致模块装错环境、PyCharm里没有正确切换解释器。我把完整路径走一遍你照着做基本不会出问题。2.1 使用pip安装pytest的注意点最基础的安装命令pip install pytest安装完成后验证pytest --version如果出现pytest: command not found或者提示版本不是最新大概率是两个原因一是当前环境里pip对应的Python版本和你写代码用的不是同一个二是pytest通过pip安装到了用户目录但没有加入PATH。稳妥的做法是先确认解释器python -m pip --version python -m pip install pytest python -m pytest --version用python -m的方式执行可以保证安装和执行走的是同一个解释器。我建议所有项目里的测试命令都用这种写法能避开很多环境错乱问题。另外如果你的网络环境访问默认PyPI源比较慢可以临时指定一个镜像源python -m pip install pytest -i https://pypi.tuna.tsinghua.edu.cn/simple镜像源只是加速手段不影响框架本身功能。装完之后测试一下导入是否正常python -c import pytest; print(pytest.__version__)2.2 PyCharm里跑pytest的配置细节PyCharm对pytest的支持很成熟前提是把项目解释器配好。打开PyCharm的Settings进入Project处的Python Interpreter面板确认当前项目用了哪个解释器。然后在同一个面板右侧点击加号搜索pytest直接安装或者你已经在命令行装好了这里只需要确认它出现在已安装列表里。接下来设置默认测试运行器。在Settings里搜索Default test runner把选项从Unittests切换为pytest。这一步很关键不设置的话PyCharm会按unittest的规则去识别你的test_文件虽然也能跑但右键运行的方式和命令行行为会有细微差别比如参数化用例展示层级不一样。设置完成之后在test_开头的函数或者类文件上右键选择Run pytestPyCharm的Running窗口会以列表形式展示每条用例的结果失败用例直接点击Traceback跳转到对应代码行。2.3 pytest的用例收集规则为什么值得当重点记pytest默认的测试发现规则是从当前目录递归搜索匹配test_*.py或*_test.py的文件文件名命中的文件里再找test_开头的函数以及Test开头的类中test_开头的方法。我见过很多人在命名上随意比如模块叫check_login.py函数叫validate_login结果pytest一条都收集不到还以为是框架坏了。其实规则不是pytest限制你而是帮你减少配置成本——只要你遵守约定它就能自动找到所有用例。如果你确实有特殊命名需求可以在pytest.ini里调整收集规则[pytest] python_files *_test.py test_*.py check_*.py python_functions test_* *_check2.4 第一个用例跑通一个真正的断言失败过程环境做好准备后我们写第一个例子。创建一个项目目录结构是这样的demo_project/ ├── test_calc.pytest_calc.py内容def add(a, b): return a b def test_add_positive(): assert add(1, 2) 3 def test_add_zero(): assert add(0, 10) 10 def test_add_fail_case(): assert add(2, 2) 5 # 这个用例会失败在终端执行python -m pytest输出中会看到2 passed, 1 failed。失败的那个用例会显示红色的F标识并展开具体信息 assert add(2, 2) 5 E assert 4 5 E -4 E 5这个输出是pytest非常有价值的地方它不光告诉你断言失败还把两边实际值都列出来-4是实际值5是期望值。排错效率比unittest那种只告诉你AssertionError高太多。2.5 常用命令行参数先记这几个入门阶段不需要把所有命令都背下来我建议先记住这几个高频参数参数作用示例-v显示详细用例列表pytest -v-k按关键字筛选用例pytest -k login-x遇到第一个失败就停止pytest -x--maxfail2允许最多失败2次pytest --maxfail2-s显示print输出pytest -s--lf上次失败用例优先重跑pytest --lf-q简洁输出pytest -q其中-k配合-s在调试阶段特别好用比如你只关心登录模块python -m pytest -v -k login -s3. 断言和fixturepytest两大基本功的进阶玩法跑通基础之后熟悉pytest的日常写法和核心机制才算真正进入框架。3.1 断言不只是assert True很多人以为pytest的断言就是assert 表达式其实它有更丰富的形态。第一种是异常断言测试某个操作应该抛特定异常import pytest def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): 1 / 0pytest.raises还能进一步检查异常内容def test_error_message(): with pytest.raises(ValueError, matchinvalid amount): withdraw_money(-100)match参数支持正则匹配这在实际接口测试里很常用比如校验参数校验失败的错误提示文案。第二种是断言集合包含关系def test_user_list(): user_list get_users() assert admin in user_list assert len(user_list) 10原生Pythonin、not in、大小比较都能直接用比记忆一堆API舒服多了。第三种是给断言语句追加说明信息def test_order_status(): status query_order(A123) assert status PAID, forder A123 status is {status}, expected PAID这个技巧在实际项目里帮过我大忙。测试用例数量一多裸断言很难一眼看出业务语义加上描述信息后失败输出直接告诉你单子A123当前状态是XX期望是PAID排查问题的成本肉眼可见地下降。3.2 fixture的依赖注入理解它的核心价值fixture是pytest区别于unittest最明显的地方它的设计思想是依赖注入。你不需要在执行前调用准备工作只需要在函数参数里声明你要用什么fixturepytest会自动找到并执行它。一个最简单的fixtureimport pytest pytest.fixture def database_connection(): # 模拟建立连接 conn create_connection() return conn def test_query(database_connection): result database_connection.query(SELECT 1) assert result 1这里test_query里使用参数database_connection后pytest会在执行测试函数前自动调用fixture函数把返回值传给测试函数。相比unittest的setUp不需要继承不需要self变量传递职责边界更清晰。fixture真正厉害的地方在于scope作用域控制pytest.fixture(scopesession) def global_config(): # 整个测试会话只创建一次 return load_config() pytest.fixture(scopemodule) def module_db(): # 每个模块只创建一次 return connect_db() pytest.fixture(scopefunction, autouseTrue) def record_timer(): # 每个函数自动使用不需要显式声明 start_time time.time() yield end_time time.time() print(fcost: {end_time - start_time}s)四种常用作用域整理如下scope执行范围典型用途function每个测试函数一次默认大部分普通场景class每个测试类一次类级别的共享资源module每个测试模块一次模块内共享连接session整个测试会话一次全局配置、环境数据、单次登录tokenscope用得好的情况下一个接口测试套件里的大量重复初始化逻辑可以缩减到几行配置。比如登录状态获取可以做成session级别的fixture整个测试过程只用登录一次而不是每个用例都走一遍登录接口。3.3 conftest.py公共fixture的集中管理仓库你可能有这样的需求多个测试文件需要共用一个fixture。如果把fixture定义在每个文件里就会产生重复代码。pytest的解决方案是conftest.py。conftest.py放在测试目录下它不会被当作用例文件收集但pytest会自动加载其中的fixture和钩子函数。目录结构这样放project/ ├── conftest.py # 全局fixture目录 ├── test_api/ │ ├── conftest.py # api子模块专属fixture │ └── test_login.py └── test_web/ └── test_ui.py父级conftest.py里的fixture对子目录的测试文件同样生效。层级作用域从近到远子目录同名fixture会覆盖父级定义。这个机制非常适合team规范化管理通用性的登录、数据清理、环境变量统一放顶层模块相关的临时数据放各目录自己的conftest.py。3.4 带yield的fixture把后置清理放回同一处unittest的tearDown要做的事在pytest里可以通过yield完成。fixture函数执行到yield时把控制权交还给测试函数测试函数执行完毕回到yield之后继续执行这部分代码就成了后置清理。pytest.fixture def create_user(): user create_user_in_db() yield user delete_user_from_db(user.id)这个写法的核心价值在于前置和数据清理放在同一个fixture里一个测试函数想用就直接声明参数不想用就不影响其他测试。相比setUp和tearDown拆成两个方法这种用一个fixture绑定整个生命周期的模式更贴近真实业务的资源管理逻辑。4. 参数化测试一条用例覆盖多组数据的正确姿势实际项目里最折磨人的测试场景之一是同一个接口要验证大量输入组合。最笨的办法是复制粘贴用例每个输入组合写一个test函数代码量爆炸且维护困难。pytest的parametrize就是专门解决这个问题的。4.1 为什么参数化测试能显著减少重复我拿登录接口举例。一个登录接口至少有正常账号密码、错误密码、账号不存在、参数缺失、密码为空、特殊字符等场景。如果没有参数化你可能要写六七个近乎相同的测试函数每个函数里的断言逻辑几乎一样只是输入输出不同。参数化写法把测试逻辑和测试数据分离一份逻辑、多组数据一眼就能看出覆盖情况import pytest pytest.mark.parametrize(username, password, expected_status, [ (admin, 123456, 200), (admin, wrong, 401), (nonexist, 123456, 401), (, 123456, 400), (admin, , 400), (script, 123456, 400), ]) def test_login(username, password, expected_status): result login(username, password) assert result.status_code expected_status执行结果会把每组数据当作独立用例展示pytest的-v输出里能看到6条用例每条都有不同的参数组合名称。跑完之后哪一组数据失败一目了然。4.2 一组参数的传递细节字典、列表和间接参数parametrize的参数名可以是多个第一个参数用字符串描述参数名多个参数名用逗号分隔。第二个参数是一组组数据每一组是一个元组元组内元素顺序对应第一个参数里的参数名顺序。如果你的数据是列表套字典也可以直接这样写pytest.mark.parametrize(login_data, [ {username: admin, password: 123456, expected: 200}, {username: admin, password: bad, expected: 401}, ]) def test_login_dict(login_data): result login(login_data[username], login_data[password]) assert result.status_code login_data[expected]还有一种情况是参数值本身比较复杂比如它需要由另一个fixture先加工完成。这一步就是indirect参数pytest.fixture def user_info(request): # fixture内部可以根据request.param返回不同结果 if request.param admin: return {role: admin, permissions: [all]} return {role: guest, permissions: []} pytest.mark.parametrize(user_info, [admin, ]) def test_permission(user_info): assert user_info[role] admin它的场景不多但在需要数据先经过fixture预处理再进入测试的场景下很实用。我建议先掌握前两种最常用的写法indirect用到时再查即可。4.3 参数化与fixture组合的常见团队模式参数化用在接口测试里有一种组合方案很实用fixture负责准备环境和token参数化负责多组业务数据。pytest.fixture def auth_token(): return get_token(admin, test-env) pytest.mark.parametrize(order_id, expected_status, [ (1001, PAID), (1002, CANCELLED), ]) def test_order_status_with_token(auth_token, order_id, expected_status): header {Authorization: fBearer {auth_token}} result query_order(order_id, headersheader) assert result.status expected_status测试函数里同时声明auth_token参数和order_id/expected_status参数pytest会先取fixture再注入参数化数据。这个模式很受团队欢迎的原因在于公共前置只写一次多组业务数据只通过一行参数列表就能直观平铺。5. 插件生态从pytest-html到allure报告的一站式接入只靠pytest核心功能已经能覆盖多数自动化测试场景。但要让它融入团队日常工作流插件和报告体系不可或缺。这里说一下我实际用下来最有价值的几个插件和allure报告的接入方法。5.1 工程中最常用的pytest插件清单以下表格是我在多个项目里实际用过的插件分享给大家做参考插件用途安装命令pytest-html生成HTML测试报告pip install pytest-htmlpytest-xdist并行执行用例加快速度pip install pytest-xdistpytest-rerunfailures失败用例自动重跑pip install pytest-rerunfailurespytest-timeout用例超时控制pip install pytest-timeoutpytest-ordering指定用例执行顺序pip install pytest-orderingpytest-random-order随机打乱用例顺序pip install pytest-random-orderpytest-cov代码覆盖率统计pip install pytest-cov5.2 最简单的报告方案pytest-html不需要额外部署的话pytest-html是性价比很高的选择。安装后执行命令python -m pytest --htmlreport.html执行完毕会在当前目录生成report.html直接用浏览器打开里面有每一条用例的通过/失败状态、时长、错误堆栈还带按模块分类的汇总区域。CI里可以把report.html作为构建产物存档。它的缺点也很明显样式比较朴素失败截图这类自定义内容需要自己扩展。如果你需要更专业的可视化报告就需要上allure了。我个人的习惯是项目前期都用pytest-html过渡一旦有分享、追溯、历史对比的需求就直接切allure。5.3 allure报告的安装与命令行接入流程allure报告要分两层第一层是Python侧的allure-pytest插件负责在测试执行时生成json临时结果文件第二层是allure命令行工具负责把json结果渲染成可视化的HTML报告。安装插件python -m pip install allure-pytest然后写用例时通过allure的装饰器为用例添加描述信息import allure allure.feature(登录模块) allure.story(正常登录) allure.title(正确账号密码登录成功) allure.severity(allure.severity_level.BLOCKER) def test_login_success(): assert login(admin, 123456) 200执行时指定结果输出目录python -m pytest --alluredir./allure-results执行完成后用allure命令行生成HTML报告allure generate ./allure-results -o ./allure-report --clean打开报告allure open ./allure-reportallure报告首页有非常完整的Dashboard能看到总用例数、通过率、重度缺陷数量、执行时长走势。每个用例详情页会展示前置条件、步骤、参数、附加信息可以把接口请求和响应内容直接附到allure.step里团队成员在排查失败用例时基本不需要再翻原始日志。这里需要注意一点allure命令行工具本身需要单独安装它不是pip包。在Windows上可以用scoopscoop install allure在macOS上可以用homebrewbrew install allure如果你暂时不方便安装allure命令行只想先看效果也可以用allure-pytest生成json结果后配合一些第三方工具渲染。但长期来看直接装好allure命令行是正路。5.4 接入allure之后的用例描述规范团队使用allure时最容易出现的问题是每个用例都有装饰器但写得非常随意。比如feature写成测试story写成一长段话title不加任何信息最后报告呈现出来就是信息废墟。我定的一个简单规范feature写业务模块story写分支场景title写条件行为预期结果。示例allure.feature(订单中心) allure.story(订单状态查询) allure.title(已支付订单查询状态下单状态为PAID) def test_paid_order_query(): ...这样报告在allure里能直接当一个可读的业务测试清单用非技术成员打开也能看懂什么场景被覆盖了。6. 踩坑记录assert重写机制、失败重试与并行执行的兼容问题优秀框架也有坑pytest的坑多数不是bug而是使用方式触发了底层机制的边界。这里记录三个我多次遇到、也帮同事排查过的实战问题直接列出排查链路大家遇到类似情况可以按这个思路来。6.1 pytest的assert重写机制和它的副作用pytest为了让assert语句能展示详细的失败上下文会在用例加载时对assert表达式做AST级别的重写把assert expr改写成一个能捕获左右值的带上下文断言。这个机制是pytest在收集用例时自动完成的所以命令行跑测试时一切正常但有些情况下会失效。最容易踩到的场景是把pytest的测试文件在调试模式下手动调用比如用python直接运行一个测试函数或者在某些IDE的Python Console里执行pytest框架收集到的函数。这时没有走pytest的收集流程assert重写不生效失败输出就只剩一行AssertionError看不到左右值。遇到这种情况不用慌排查步骤很简单确认是不是用python直接执行了测试文件如果是改用python -m pytest执行确实需要在脚本内调用pytest时用pytest.main()而不是手动import测试模块里的函数。# 正确做法 import pytest if __name__ __main__: pytest.main([-v, test_calc.py])6.2 断言信息里的中文乱码问题Windows终端下跑pytest失败信息里包含中文时偶尔会出现乱码或者控制台无输出这个问题的根源在于默认编码不是UTF-8。遇到时第一步检查环境python -c import sys; print(sys.stdout.encoding)如果显示的是gbk或cp936执行时强制使用UTF-8编码python -X utf8 -m pytest或者在pytest.ini里配置[pytest] addopts -X utf8配置addopts时要注意的是它会影响所有用pytest命令执行的流程不太建议在addopts里放太多个性化参数容易造成多个命令叠加后的意外行为。如果你只是想让报告里的中文正常显示设置好代码文件头部的coding声明、统一用UTF-8保存文件、日志里打印时用repr做兜底这三步基本能解决90%问题。6.3 失败重试插件与xdist并行执行的实际冲突项目用例多之后大家会想用pytest-xdist做并行同时又希望失败用例能自动重试两个插件一起装、一起用结果发现重试行为变得非常诡异甚至某些用例直接不执行了。这个问题的根源在于两者的协作方式。pytest-xdist会把用例分发到多个worker进程每个worker本地维护自己的用例执行状态。pytest-rerunfailures的重试机制是修改用例的执行结果尝试在同一个进程里再次运行该用例。两个机制叠加时某些版本的插件并不能在分布式环境下很好的配合行为就会不可预期。我的解决方案是需要重试的场景不要和并行同时开启或者给重试用例单独打标记通过-m筛选后单独跑。# 并行执行不加重试 python -m pytest -n 4 # 重试执行不并行 python -m pytest --rerunfailures2 -k smoke如果你真的需要并行重试同时承载大规模回归建议升级到支持良好兼容性的插件版本并且先在两个worker的小范围测试里验证重试行为再全量推广。6.4 不要让测试用例之间产生顺序依赖我这次想额外说一个教训它不算是pytest的报错但踩过坑的人都懂。如果你在用例里直接依赖另一个用例的执行结果比如用某个全局变量在test_a里赋值、在test_b里读取那么参数化、并行、失败重试都会失效甚至出现随机失败。pytest对用例的执行顺序并没有强约定并行时不同进程的执行顺序更不可控。正确做法是把跨用例的数据依赖改为fixture生成需要登录态就写一个fixture返回token谁用谁声明不通过用例间传递。这样你的每个用例都是独立可重复执行的整个测试套件的稳定性会提升一个量级。6.5 让pytest成为团队的默认约定最后分享一个团队落地的小习惯。一开始我们只是把pytest当成比unittest高级一点的工具跑了一两周后发现它值钱的不是工具本身而是大家一起定下的测试约定文件名必须test_开头、所有公共fixture进conftest.py、业务模块统一用allure.feature标注、断言语句带上业务描述。这些约定配合pytest的机制变成了团队代码评审时的检查项。每个新同事进来只需要花一小时读一遍conftest.py和几个典型用例再照着写一个新的测试函数提交基本上不需要额外教。而过去用unittest时光是解释setUp和tearDown的继承逻辑就要花半天。这就是我强调的优雅——它让团队协作的摩擦降到了最低。如果你正在考虑要不要把现有项目切到pytest我的建议是别大动干戈重构存量用例。可以先在新增模块用pytest写跑稳定之后再逐步迁移旧用例毕竟pytest连unittest风格的用例也能直接收集执行兼容成本比想象中低得多。
返回列表