ARTICLE DETAIL

资讯详情

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

Pytest自动化测试实战:从fixture到Allure报告与CI集成

Pytest自动化测试实战:从fixture到Allure报告与CI集成 从我在一家电商公司第一次正经搭建自动化测试框架开始Pytest就成了我工具箱里最顺手的那个工具。前后用纯Python做过UI自动化、接口自动化也折腾过unittest、nose、Robot Framework后来几乎所有新项目我都会毫不犹豫选Pytest。如果你正准备从零学自动化测试或者正被unittest那一套类继承写法搞得头皮发麻这篇分享应该能帮你在选型和落地阶段少走不少弯路。我会把这几年用Pytest做接口自动化和UI自动化的真实经验拆开讲包括框架选型、核心机制、完整工程结构、Allure报告接入以及一堆只有踩过坑才知道的细节。1. 为什么是Pytest一次真实的选型复盘1.1 我从unittest迁移过来的真实原因早些年我团队里的自动化测试脚本基本都跑在unittest上。接触新需求时流程一般是写TestCase类、定义setUp和tearDown、用assertEqual断言、最后套一个测试套件去执行。这套写法本身没毛病尤其在Python标准库还没有被各种第三方框架挤压的年代unittest是稳妥的默认选项。但等我真正维护过几套超过两万行的自动化用例后痛点就非常具体了第一类继承带来的结构僵化一个用例要复用另一段初始化逻辑就得抽公共基类或硬改继承关系改动一点牵动一片第二断言方法种类有限assertEqual、assertTrue这些在面对复杂JSON结构、响应体校验时写起来非常啰嗦第三没有原生的参数化和数据驱动支持跑多组数据只能靠循环拼接用例名出了失败信息也不直观。有一次我要验证一个下单接口的13组边界参数用unittest写出来后每一个用例都是重复的类和方法代码冗余不说跑挂了还要去定位是第几组数据出的问题。也是在那个节点我认真对比了Pytest发现它允许我用最朴素的函数去写用例直接使用Python原生的assert关键字做断言失败时能自动展开表达式的左右值让错误信息精确到具体哪一个值不对。这个体验对我这种常年对着终端log查问题的人来说几乎是降维打击。1.2 Pytest真正打动我的三个特性先说fixture机制。这是Pytest区别于unittest最核心的一点也可以理解成依赖注入式的初始化管理。你不必非要继承什么基类也不用在每个类里写setUp只需要定义一个普通函数并加pytest.fixture装饰器哪个用例需要就在参数里声明这个fixture的名字Pytest会自动把返回值注入进去。初听起来像是把setup换了个写法但实际用上scope、yield、autouse这些参数后你会发现它能做的事情远不止初始化。第二是参数化能力也就是pytest.mark.parametrize。这是让我把13组边界参数从噩梦变成正常需求的关键。一组参数就是一组独立的用例数据Pytest会为每一组数据生成独立的测试节点执行结果、失败定位都能单独展示不用自己拼用例名、不用写循环、不用额外收log。第三是生态和插件体系。Allure报告、失败重试、执行顺序控制、并发执行、分布式调度这些在工程化里绕不开的需求Pytest几乎都有对应插件。别的框架需要自己造轮子或者干脆不提供的能力在Pytest里往往是pip install一个包、加两行配置的事。对于一个程序员省下来的时间就是用来处理业务逻辑的。提示如果你只想在测试金字塔里找一个稳定、社区活跃、资料多的框架Pytest是目前Python阵营里最不需要犹豫的选择。2. 先把核心机制吃透fixture、参数化与断言2.1 fixture不只是setup/teardown的替代品我把fixture理解成一个带返回值的、可复用、可带作用域的初始化和清理器。它最简单的用法是定义一个函数用pytest.fixture标记在里面创建好数据或客户端对象然后用return或yield返回。用例函数签名里写上对应参数名Pytest就会在执行前调用这个fixture并把返回值传进用例。这里面用法差别巨大的是scope参数。默认是function也就是每个用例执行前都会跑一次改成class类里的所有用例共享一次改成module整个模块共享一次改成session则整个测试会话期间只执行一次。很多人容易在这里踩坑有一个接口登录态如果让每个用例都重新登录一次执行时间翻倍但如果把scope直接设成session又可能出现token过期、共享数据被某条用例修改后影响后续用例的问题。我通常的建议是取决于操作的成本和可变性比如登录token获取成本高并且内容稳定就放session scope而一些临时造数的操作用function scope反而更安全。需要执行清理时在fixture里用yield返回yield后面的代码在用例结束后执行等价于自带teardown。fixture还有一个容易被忽略的玩法是autouse。你把autouseTrue写到装饰器里不需要在用例参数里声明Pytest会在匹配的范围内自动为每个用例执行该fixture。适合做全局埋点、会话初始化、环境切换之类的事。但autouse也别用太狠如果你的fixture做了重操作比如起了数据库连接、加载了完整模型全自动执行会显著拖慢整体速度而且出了问题不好定位。2.2 参数化一条用例跑遍全场景pytest.mark.parametrize是Pytest最常被我使用的装饰器之一。用法很直白第一个参数是参数名字符串多个参数用逗号分隔第二个参数是一个列表列表里每一项就是一组测试数据。它适合三种典型场景接口边界值校验、多环境多账号的回归、同一业务规则下不同输入组合的验证。等我真正把参数化用熟练后发现几个容易忽略的细节。一是ids参数默认情况下Pytest会用数据项本身的字符串表示作为用例节点ID如果你传入的是复杂对象、含中文的字典终端和报告里的用例名会非常难看。用ids可以自定义一组简短、可读的用例名类似登录成功_手机号、登录失败_密码错误这种执行结果一眼看懂。二是parametrize可以叠加多个装饰器实现笛卡尔积式的数据组合两个装饰器会生成所有组合的用例这在做兼容性测试时很好用。三是indirect参数它允许参数化直接把数据传进fixture而不是用例本身适合那些需要根据参数动态准备环境或数据的场景。有人会问参数化是不是越多越好我的经验是数据驱动要做但每一条数据都要有业务意义。两条一模一样的用例只有测试数据数值不同、覆盖的边界相同其实是在浪费执行时间。一次我接手一套老用例一个下单接口参数化了两百多组数据实际跑完大部分是在同一个逻辑分支里打转后来我把数据按正常流-异常流-边界值-权限校验分类压缩到六十多组回归时间直接降了一半覆盖效果反而更清晰。这也是我对参数化一个很深的体会数据驱动服务于业务覆盖不是为了凑用例数。2.3 断言与标记让失败信息有真正价值Pytest支持直接用Python的assert关键字这背后有一个很实用的机制叫断言自省。当你写assert a b失败时Pytest会自动解析这条表达式把a的实际值和b的实际值分别展示出来。这比assertEqual更好用的地方是它不会限制你的断言方式你可以自由地写assert user[id] 0、assert success in resp[msg]、assert isinstance(resp[data], list)这种更贴近业务表述的断言。遇到复杂JSON响应我还会封装一个小的断言工具函数比如assert_json_contains、assert_time_range让用例代码更简洁。标记mark机制也是我从unittest迁移后觉得顺手的地方。比如pytest.mark.smoke标记冒烟用例运行的时候用-m smoke就能只跑这类用例用pytest.mark.run(order1)可以控制用例执行顺序来自pytest-ordering插件用pytest.mark.skipif可以按环境、版本、外部条件动态跳过不满足条件的用例。它让用例的管理粒度从文件级别下沉到了用例级别这在维护大型测试集的时候价值非常大。从工程化角度看标记可以承担执行策略的功能你不用写复杂的shell脚本去挑选要执行的用例一行-m参数就搞定了。3. 接口自动化实战搭一个能直接跑的测试工程3.1 目录设计从第一天就不要乱我在很多项目里看到过这样的测试目录所有脚本平铺在test_case下一个文件动辄上千行工具函数、用例、配置全混在一起。短时间没问题但用例一多维护成本会指数上升。我目前比较习惯的目录结构如下project_root/ ├── config/ # 配置文件与环境差异 │ ├── dev.ini │ ├── staging.ini │ └── read_config.py ├── api/ # 接口封装层 │ ├── __init__.py │ ├── base_api.py │ └── order_api.py ├── testcases/ # 测试用例层 │ ├── test_order_flow.py │ └── test_user_login.py ├── conftest.py # 根级fixture ├── pytest.ini # pytest配置 ├── requirements.txt └── reports/ # 报告输出目录这个结构分了三层目的是让接口定义、测试逻辑、数据配置三者解耦。api层负责HTTP请求细节和响应包装testcases层只关心业务场景和断言config层管理环境差异比如dev和staging的BaseURL、账号、超时时间不一样都从配置读取。测试用例本身不直接拼URL、不硬编码密码。这样做的好处是换环境时只改配置不改用例后端接口字段变了只改api层用例仍旧稳定。有一个原则我一直坚持测试代码里不允许出现写死的IP地址和密钥出现一次就是一次技术债迟早会还。3.2 写一条完整的接口用例登录token关联我直接用一个非常常见的场景演示登录接口获取token然后用token去查用户信息。很多初学者会用全局变量保存token这样做在单用例没问题但用例一旦开始并发执行、数据被反复覆盖就会出各种诡异问题。我更建议用fixture把登录和token管理封装起来。先写api层的一个登录封装import requests class UserApi: BASE_URL https://api.example.com def __init__(self): self.session requests.Session() def login(self, username: str, password: str) - dict: resp self.session.post( f{self.BASE_URL}/login, json{username: username, password: password}, timeout10, ) resp.raise_for_status() return resp.json() def get_user_info(self, token: str) - dict: resp self.session.get( f{self.BASE_URL}/user/info, headers{Authorization: fBearer {token}}, timeout10, ) resp.raise_for_status() return resp.json()这里用requests.Session而不是直接requests.get是因为Session会自动管理连接池在同一个会话内的多次请求可以复用TCP连接接口数量多时效率提升明显。接下来在conftest.py里定义一个session级别的登录fixtureimport pytest from api.user_api import UserApi pytest.fixture(scopesession) def login_token(): api UserApi() resp api.login(test_user, pwd123456) assert resp[code] 0, f登录失败: {resp} return resp[data][token]然后在用例文件里直接引用import pytest from api.user_api import UserApi def test_get_user_info(login_token): api UserApi() info api.get_user_info(login_token) assert info[code] 0 assert info[data][username] test_user assert isinstance(info[data][balance], (int, float))这条用例会有两个阶段先跑session级别的login_token再跑用例本身。由于token是session级共享整个测试会话只登录一次后面的同类用例都复用这个token。但要注意如果测试用例中有一条会主动登出、改密码token就会失效这时你必须给那条用例打标记、把它排到最后或者让该模块单独使用带function scope的独立token。这属于共享数据带来的典型风险具体怎么处理我在后面第5章再展开。3.3 conftest.py 与全局配置的正确用法conftest.py是Pytest里一个非常特殊的文件Pytest会自动发现它并加载里面的fixture和钩子不需要显式import。正因为这个自动加载机制很多人会往里堆一堆fixture最后整个文件几百行什么都有。我的建议是conftest.py按层级放置根目录放全局必须的fixture比如环境变量加载、统一日志、全局登录tokentestcases下某一子目录再放只属于该子模块的fixture。这样fixture的作用范围清晰不会出现在订单模块里被一个登录fixture意外干扰的问题。fixture本身也可以读取配置。我在项目里会写一个read_config小模块把dev.ini和staging.ini里的环境信息读成字典然后在conftest里动态选择当前环境。选择环境的惯用做法是命令行参数配合pytest_addoption钩子比如def pytest_addoption(parser): parser.addoption(--env, actionstore, defaultdev, help选择测试环境: dev/staging) pytest.fixture(scopesession) def env_config(request): env request.config.getoption(--env) config read_config(env) return config这样执行pytest --env staging时所有用例自动切换到staging环境。这里有一个我自己比较强调的细节就是环境切换信息要尽量收敛不要让每个用例都去判断环境。用例只应该看到env_config里的最终数据比如base_url、账号、超时时间至于这些数据从哪个配置来它不关心。这能让用例写得很干净。3.4 pytest.ini别再用命令行堆参数很多初学者喜欢在命令行里写一长串参数pytest -v -s --tbshort --htmlreport.html testcases/test_login.py。这样每次运行都要敲一遍既容易漏参数也容易造成团队成员执行结果不一致。我强烈建议把公共配置写进pytest.ini让执行命令短到只有pytest一个词。一个典型的pytest.ini长这样[pytest] testpaths testcases addopts -v -s --tbshort --strict-markers markers smoke: 冒烟测试用例 slow: 慢速用例 api: 接口用例 ui: UI用例 filterwarnings ignore::DeprecationWarning这里的testpaths指定了收集用例的目录addopts是默认追加的命令行参数--strict-markers会强制你注册所有marker一旦用例里写了未注册的markerPytest会直接报错这个机制能提前拦住很多拼写错误。markers列表里我还会用中文备注每个标记的用途方便团队新人理解。此外如果项目里有大量的用例文件和目录我通常还会在pytest.ini里配置log_clitrue让日志直接输出到控制台加上log_cli_levelINFO排查问题的时候不用反复去翻report文件。有人会问pytest.ini和conftest.py会不会职责重叠我的经验是pytest.ini管的是Pytest框架自身的行为比如收集规则、命令行默认值、标记注册conftest.py管的是测试逻辑层面的东西fixture、钩子、环境准备。职责分开维护起来就不容易乱。4. 进阶能力报告、重试与CI联动4.1 接入Allure让报告成为团队的通用语言接口自动化跑起来后最重要的事情是让结果能被所有人看到。Allure是我目前用得最顺手的报告工具它和Pytest的配合已经非常成熟。接入过程分三步安装依赖、本地装Allure命令行工具、在pytest.ini的addopts里加上--alluredir./reports/allure-results。为了让报告在业务层面更可读我会在用例上打一些Allure注解比如allure.feature标注模块、allure.story标注功能点、allure.title写当前用例的业务标题、allure.description写必要的前置条件和预期结果。这样报告打开就是按业务模块分层的管理人员看feature维度测试人员看story和title维度不用去翻代码。一个简单的示例import allure allure.feature(订单模块) allure.story(创建订单) allure.title(正常创建订单-余额充足) def test_create_order_success(login_token, env_config): ...Allure报告还有一个非常好用的能力是step和attachment你可以用with allure.step(准备商品数据):这样的上下文把关键操作分成步骤用allure.attach把请求日志、响应体、失败截图挂到报告里。这条经验对排查现场问题特别有用尤其接口自动化里你根本看不到浏览器界面如果报告里只有一句断言失败排查起来非常痛苦但如果你附上了完整的请求头、请求体、响应JSON基本就可以直接定位是参数问题还是服务端异常。我通常会在API封装层统一把请求和响应记录成JSON字符串再通过allure.attach挂载这样所有用例自动获得完整的请求上下文不需要每个用例单独写。4.2 失败重试与用例筛选执行的取舍接口测试在网上跑的时候最让人头疼的不是功能bug而是网络抖动、服务端偶发超时导致的用例误报。误报太多团队就会对自动化结果失去信任最后报告也没人看。解决误报pytest-rerunfailures插件是个好帮手配置也比较简单比如在命令行或addopts里加--reruns2 --reruns-delay1表示失败后重试两次每次间隔1秒。但我要特别提醒一点重试不是越多越好。重试次数设太高用例整体执行时间会翻倍如果服务端确实有问题重试三次的结果和三十分钟之后重试三次的结果基本一样。而且对于确定性数据污染的问题重试再多次也没用反而会掩盖真实问题。我一般只在可能受网络波动影响的用例或者慢接口上配置重试重试次数不超过2次。某些对幂等性要求很高的场景比如创建订单、扣款操作重试要格外谨慎如果接口本身没有做到幂等重试可能产生重复数据。此时更稳妥的办法是先在用例里做好前置清理而不是依赖重试。用例筛选执行方面pytest提供的-m参数配合markers基本可以满足90%场景。比如在CI里提交代码后的冒烟任务只跑pytest -m smoke每日回归任务跑pytest -m not slow排除掉耗时的慢用例发布前完整回归则不筛选全量执行。再加上pytest-ordering控制关键用例顺序、pytest-xdist做多进程并行-n auto我可以很灵活地组合执行策略。这里有一个特别容易踩的坑需要注意pytest-xdist的并发模式下session级fixture在每个worker里都会执行一次并不是进程间共享所以如果登录token的生成有并发安全问题就需要在fixture里做锁或者改用module级别否则并发跑了几个workertoken就可能互相覆盖。4.3 把pytest塞进流水线的几个要点自动化测试最终要产生价值必须进入CI流水线让它在每次代码变更后自动执行。我常用的是在CI的job里先安装依赖再启动测试服务或依赖的中间件然后执行pytest最后上传Allure报告。整个流水线里我会刻意让自动化测试阶段尽早失败。意思是如果冒烟测试只有30条用例它就应该比全量回归更早执行一旦冒烟挂掉整个流水线直接红色避免后续任务白白跑。这里还有一个不常被提到的经验测试过程的稳定性和可重复性比断言覆盖率更重要。一个每周都因为环境问题随机失败的流水线很快就会被团队悄悄关掉。所以我强烈建议在流水线里加入环境自检步骤比如先跑一个健康检查脚本确认数据库连通、依赖服务可用再进行真正的用例执行。这样能区分是环境挂了还是用例挂了。执行结束后即使用例全部通过也要把测试报告归档成带时间戳的产物方便后续回溯某次发布时那一轮测试到底跑了什么。我认为这一点常常被忽略但它在工程实践里非常值钱。5. 常见问题与排查技巧实录5.1 fixture作用域共享引发的数据串扰我遇到过一个很典型的线上事故登录fixture用了session级别测试集并发跑了三个worker结果其中一个worker里有一条用例改了用户名头像后续另一个worker的用例读取用户信息时就一直失败但单独跑又全部通过。这就是共享数据串扰。排查思路是先看是不是并发导致的隔离问题再把fixture的scope改成module或function做对照实验。我最终的解决方案是让每个worker使用不同的测试账号账号数据在conftest里按worker序号做映射这样既能保留session级别的性能优势又隔离了数据。这个排查过程非常能体现Pytest机制的理解深度建议团队里的人都亲手试一次。还有一个常见现象是同一个module下的多条用例共享了一个可变的list或dict类型的fixture返回值某条用例修改了它后面的用例就收到被改过的数据。解决起来很简单fixture返回可变对象时不要直接暴露给用例而是通过工厂模式或者copy一份。我的习惯是返回不可变配置需要变动的场景就使用工厂式fixture比如def make_order_data(): return {...}而不是直接返回一个对象。5.2 参数化数据驱动时编码与ID显示问题参数化的数据如果包含中文旧版本Pytest在终端和控制台输出时有时会显示成unicode转义报告里一坨\u4e2d\u6587完全没法看。这个问题多数和执行环境的编码有关。我在Windows控制台上碰到过设置PYTHONIOENCODINGutf-8或者升级到较新的Pytest版本基本能解决。另外当参数化数据项本身是一个dict如果不设置ids报告里的用例ID会显示成字典对象的字符串表示极难阅读。我会给每一组数据都制定一个业务可读的ID比如登录成功-正确密码。这不仅是报告美观问题更是可维护性问题测试用例的名字就是它们对外交流的语言。5.3 用例顺序依赖与执行环境隔离Pytest默认按照文件内定义的顺序执行用例但如果你依赖某条用例先跑、给后面用例创造了数据这套逻辑在xdist并发下会直接崩掉因为不同文件的执行顺序不受控制。我的处理原则是能用前置数据准备解决的就不要依赖用例执行顺序。比如需要一条已存在的订单数据就在用例里通过接口或数据库脚本创建而不是让另一条创建订单的用例先跑。确实需要排序的场景比如支付流程就到api层封装好完整的事务操作让每条测试代表独立的业务验证然后只在少数依赖链上用pytest-ordering控制顺序。这样做的收益是即使删掉任意一条用例其他用例仍然可以稳定运行维护时会舒服很多。5.4 超时与重试参数怎么设才算合理超时时间我一般遵守比线上SLI稍微宽松一点的原则。接口正常情况是300ms我通常会给2到5秒如果是上传文件、导出报表这类慢接口就按业务容忍上限设置比如10到30秒并配合重试机制。重试间隔也不是固定的我更喜欢用指数退避第一次失败等1秒、第二次等2秒、第三次等4秒这能尽量避免重试风暴。这里要特别提醒的是超时时间不能设置成1秒因为很多服务在冷启动、缓存重建时会有明显的慢请求过短的超时会让大量用例误报。我踩过一次大坑给一个秒开接口设了1秒超时结果每天凌晨的定时任务因为实例冷启动三十多条用例全部误报后来调整到5秒并额外给这批用例加了重试误报率才归零。如果让我给一个通用的起步值我的建议是普通接口超时5秒、重试2次慢接口按业务上限再加50%的缓冲具体数值一定要实际运行几轮后稳定下来再固化。最后再分享一个我这几年的习惯每个新项目落地Pytest时我都会先花半小时把pytest.ini和conftest.py这两块地基打牢而不是急着写第一条用例。因为大部分后续的混乱都源于这两个地方职责不清。Pytest的灵活度非常高灵活意味着你可以很快写出一版能跑的脚本但也意味着一旦结构没立住后期维护就是一场漫长的消耗战。先把作用域、数据隔离、环境切换这些规矩定好再谈用例数量这是我在所有自动化项目里都坚持的原则。
返回列表