尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

pytest+requests+Allure接口自动化测试框架实战

pytest+requests+Allure接口自动化测试框架实战 1. 为什么选择 pytest requests Allure 这套组合1.1 接口自动化测试框架的选型逻辑做接口自动化绕不开三个核心问题用什么发请求、用什么组织用例、用什么出报告。市面上能解决这三个问题的工具不少但真正能长期维护、团队协作不打架的组合其实不多。我最早做接口自动化的时候用的是 Postman Newman写起来快但用例一多就崩——几百个接口的断言逻辑散落在各个 Collection 里改一个公共参数要翻十几个文件夹。后来换过 JMeter压测确实强但拿它当纯接口回归工具用脚本维护成本太高一个接口的断言要拖好几个元件团队里非测试岗的同事根本看不懂。最终落到 pytest requests Allure 这套组合原因很直接requests负责发请求语法极简一个requests.post(url, jsonpayload)就能覆盖 90% 的接口调用场景而且它对 session 管理、cookie 保持、超时重试的支持都是开箱即用的。pytest负责组织用例它的 fixture 机制是这套框架的灵魂。你可以把登录 token、数据库连接、测试数据准备这些前置动作抽成 fixture用例里只写业务断言代码干净得像在写伪代码。Allure负责出报告它生成的报告不是那种绿了红了的简陋页面而是带步骤、带附件、带请求响应日志的可视化报告。给产品经理看的时候对方能直接看到这个接口返回了什么、断言了什么、为什么失败省掉大量沟通成本。这套组合还有一个隐性优势生态兼容性极好。pytest 支持参数化、支持插件扩展、支持多线程并发requests 能无缝对接各种鉴权方式Allure 的装饰器可以嵌在 pytest 用例里三者之间没有胶水代码全是原生配合。1.2 这套框架适合谁、能解决什么问题如果你符合以下任意一种情况这套框架值得你花时间搭一遍手上有几十到几百个接口需要做回归测试每次发版都要手动跑一遍跑完还得手动整理结果团队里接口文档和测试用例是两张皮文档更新了用例没更新测试结果没人信想往 CI/CD 里塞自动化测试但现有的工具要么太重、要么报告太丑、要么和流水线集成麻烦面试时被问到你搭过接口自动化框架吗只能含糊说用过 Postman。搭完之后你能得到的东西很具体一条命令跑完全部接口用例自动生成带请求响应详情的 HTML 报告失败用例能定位到具体是哪个字段断言挂了报告可以直接归档或发给相关人。1.3 整体架构分层设计在动手写代码之前先把目录结构定下来。我见过太多人上来就写test_xxx.py写到第 20 个用例的时候发现公共方法没地方放只能复制粘贴最后整个项目变成一坨。我推荐的目录结构是这样的api_auto/ ├── common/ # 公共层 │ ├── __init__.py │ ├── request_util.py # 请求封装 │ ├── assert_util.py # 断言封装 │ ├── logger.py # 日志封装 │ └── yaml_util.py # 配置读取 ├── config/ # 配置层 │ ├── config.yaml # 环境配置 │ └── test_data/ # 测试数据 ├── testcases/ # 用例层 │ ├── conftest.py # fixture 定义 │ ├── test_user.py │ └── test_order.py ├── reports/ # 报告输出 │ ├── allure-results/ │ └── html/ ├── pytest.ini # pytest 配置 └── requirements.txt这个分层的核心思想是关注点分离common 层只管怎么发请求、怎么断言config 层只管连哪个环境、用什么数据testcases 层只管测什么业务。改环境配置不用动用例改断言逻辑不用动请求封装各改各的。注意不要把所有东西塞进一个utils.py。我踩过的坑是项目初期图省事请求封装、断言、日志全写在一个文件里三个月后这个文件 800 行谁都不敢改。2. 环境搭建与核心依赖安装2.1 Python 环境准备与版本选择Python 版本建议选 3.8 到 3.11 之间。3.8 是很多企业环境的底线3.11 是性能和兼容性比较平衡的版本。不建议用 3.12因为部分 Allure 相关依赖在 3.12 上还有兼容性问题我实测遇到过allure-pytest在 3.12 下报AttributeError的情况。安装 Python 本身没什么好说的官网下载安装包一路下一步即可但有两个细节必须注意第一安装时勾选Add Python to PATH。不勾的话后面在命令行敲python会提示找不到命令还得手动配环境变量。第二装完之后验证一下python --version pip --version两条命令都能正常输出版本号说明环境没问题。如果pip报错用python -m ensurepip修复一下。2.2 核心依赖安装与版本锁定依赖安装用 pip 就行但建议把版本号写进requirements.txt避免不同机器上装出不同版本导致行为不一致。pip install pytest7.4.3 pip install requests2.31.0 pip install allure-pytest2.13.2 pip install PyYAML6.0.1 pip install pytest-xdist3.5.0 pip install pytest-html4.1.1 pip install pytest-rerunfailures12.0逐个说一下这些包的作用包名作用是否必需pytest测试框架核心必需requestsHTTP 请求库必需allure-pytestAllure 与 pytest 的桥接插件必需PyYAML读取 yaml 配置文件推荐pytest-xdist多进程并发执行用例推荐pytest-html生成简易 HTML 报告可选pytest-rerunfailures失败用例自动重试推荐pytest-rerunfailures这个包特别值得装。接口测试天然不稳定网络抖动、服务瞬时不可用都会导致偶发失败。有了它可以在配置里指定失败用例重跑 2 次只有重跑后仍然失败才判定为真失败能大幅降低误报率。2.3 Allure 命令行工具安装allure-pytest只是 Python 侧的插件它负责生成中间结果文件JSON 格式真正把这些文件渲染成 HTML 报告的是 Allure 命令行工具。这两个东西是分开的很多人只装了前者跑完发现没有报告就是漏了这一步。Allure 命令行工具依赖 Java 环境所以先确认本机有 JDKjava -version有输出就行版本 8 以上都可以。然后去 Allure 官方发布页下载对应平台的压缩包解压后把bin目录加到系统 PATH 里。验证allure --version能输出版本号就说明装好了。提示如果公司网络下载慢可以先用pip install allure-pytest把 Python 插件装好命令行工具找同事拷一份解压即用它本质就是个 Java 程序不挑机器。2.4 pytest.ini 配置文件详解在项目根目录建一个pytest.ini这是 pytest 的全局配置入口[pytest] testpaths testcases python_files test_*.py python_classes Test* python_functions test_* addopts -vs --alluredirreports/allure-results --clean-alluredir --reruns2 --reruns-delay1 -p no:warnings逐行解释testpaths告诉 pytest 只去testcases目录找用例避免它去翻common、config这些目录浪费时间python_files用例文件名必须以test_开头这是 pytest 的默认约定写出来是为了明确addopts里的-vs-v是详细输出-s是允许 print 内容显示在控制台调试时很有用--alluredir指定 Allure 中间结果的输出目录--clean-alluredir每次跑之前清空上次的结果避免新旧数据混在一起--reruns2失败用例重跑 2 次-p no:warnings屏蔽第三方库的警告信息让控制台干净点。这个配置文件是整个框架的总开关后面所有命令都可以简化成一句pytest。3. 请求层封装与核心工具实现3.1 为什么不能直接用 requests 发请求有人会问requests 本身已经够简单了为什么还要封装一层直接用的写法是这样的import requests def test_login(): resp requests.post(http://api.example.com/login, json{user: admin, pwd: 123456}) assert resp.status_code 200 assert resp.json()[code] 0这段代码能跑但问题在于每个用例都要写完整的 URL、都要处理超时、都要手动加 header、都要自己判断响应格式。100 个用例就是 100 遍重复劳动而且一旦域名变了要改 100 个地方。封装之后用例里只需要写def test_login(): resp RequestUtil().send(post, /login, json{user: admin, pwd: 123456}) assert resp.status_code 200URL 前缀、超时、公共 header、日志记录全在封装层统一处理。这就是封装的价值把变化的部分集中管理把不变的部分留给用例。3.2 请求封装类的完整实现下面是我实际项目里用的请求封装核心思路是统一入口 自动日志 异常兜底import requests import time from common.logger import logger class RequestUtil: def __init__(self): self.session requests.Session() self.base_url http://api.example.com self.timeout 10 def send(self, method, path, **kwargs): url self.base_url path method method.upper() kwargs.setdefault(timeout, self.timeout) kwargs.setdefault(headers, {}).setdefault(Content-Type, application/json) logger.info(f请求方法: {method}) logger.info(f请求地址: {url}) logger.info(f请求参数: {kwargs.get(json) or kwargs.get(params) or kwargs.get(data)}) start time.time() try: resp self.session.request(method, url, **kwargs) except requests.exceptions.Timeout: logger.error(f请求超时: {url}) raise except requests.exceptions.ConnectionError: logger.error(f连接失败: {url}) raise cost round((time.time() - start) * 1000, 2) logger.info(f响应状态码: {resp.status_code}) logger.info(f响应内容: {resp.text[:500]}) logger.info(f耗时: {cost}ms) return resp几个关键设计点用 Session 而不是直接 requests.request。Session 会自动保持 cookie登录接口拿到的 session 在后续请求里自动带上不用手动传 token。对于需要登录态的接口测试这一点能省掉大量代码。超时时间统一设 10 秒。不设超时的请求在服务挂掉时会一直卡住整个测试套件就僵在那里。10 秒是个经验值大部分接口正常响应在 1 秒内10 秒还没返回基本可以判定异常。日志记录请求和响应。这是排查问题的命根子。用例失败时第一件事就是看请求发出去没有、参数对不对、服务返回了什么。日志里全都有不用再手动复现。响应内容截断到 500 字符。有些接口返回的 JSON 特别大全打到日志里会把文件撑爆截断前 500 字符足够定位问题。3.3 断言封装与响应校验requests 返回的resp对象本身没有断言能力pytest 的assert又太原始。我一般会封装一个断言工具把常见的校验场景固化下来class AssertUtil: staticmethod def assert_status_code(resp, expected200): assert resp.status_code expected, \ f状态码断言失败: 期望 {expected}, 实际 {resp.status_code} staticmethod def assert_json_value(resp, key, expected): actual resp.json().get(key) assert actual expected, \ f字段 [{key}] 断言失败: 期望 {expected}, 实际 {actual} staticmethod def assert_json_contains(resp, key): assert key in resp.json(), f响应中不包含字段 [{key}] staticmethod def assert_response_time(resp, max_ms2000): assert resp.elapsed.total_seconds() * 1000 max_ms, \ f响应时间超过 {max_ms}ms这样用例里写断言就是一行AssertUtil.assert_status_code(resp) AssertUtil.assert_json_value(resp, code, 0) AssertUtil.assert_json_contains(resp, data)断言失败时的报错信息也很关键。assert actual expected这种默认报错只告诉你断言失败了但不告诉你实际值是多少。加上自定义的 message失败时直接能看到期望值和实际值排查效率翻倍。3.4 配置文件与多环境切换接口测试经常要在测试环境、预发环境、生产环境之间切换。硬编码 URL 是灾难用配置文件管理才是正道。config/config.yamlenv: test test: base_url: http://test-api.example.com timeout: 10 staging: base_url: http://staging-api.example.com timeout: 15 prod: base_url: http://api.example.com timeout: 10读取配置的工具类import yaml class ConfigUtil: _config None classmethod def load(cls, pathconfig/config.yaml): if cls._config is None: with open(path, encodingutf-8) as f: cls._config yaml.safe_load(f) return cls._config classmethod def get_base_url(cls): cfg cls.load() return cfg[cfg[env]][base_url]切换环境只需要改config.yaml里的env字段或者通过环境变量覆盖。这样一套用例可以在多个环境复用不用改任何代码。实操心得生产环境的配置里base_url 建议只读不写。我见过有人在生产环境跑自动化用例结果用例里有删除操作直接把线上数据删了。生产环境跑用例前务必确认用例都是只读的查询类接口。4. pytest 用例组织与数据驱动4.1 fixture 机制把前置动作抽干净fixture 是 pytest 最强大的特性没有之一。它的作用是在用例执行前准备好某些资源用例执行后清理掉。最典型的场景是登录。几乎每个业务接口都需要登录态如果每个用例都写一遍登录逻辑代码会臃肿到无法维护。用 fixture 可以这样import pytest from common.request_util import RequestUtil pytest.fixture(scopesession) def login_token(): req RequestUtil() resp req.send(post, /login, json{user: admin, pwd: 123456}) token resp.json()[data][token] return token pytest.fixture(scopesession) def auth_header(login_token): return {Authorization: fBearer {login_token}}用例里直接声明需要auth_headerdef test_get_user_info(auth_header): resp RequestUtil().send(get, /user/info, headersauth_header) AssertUtil.assert_status_code(resp)scopesession表示这个 fixture 在整个测试会话里只执行一次。登录接口只调一次token 在所有用例间共享效率极高。fixture 的 scope 有四个级别选错了会出问题scope执行时机适用场景function每个用例执行一次需要独立数据的用例class每个测试类执行一次类内共享资源module每个模块执行一次模块级初始化session整个会话执行一次登录、数据库连接注意scope 越大共享程度越高但隔离性越差。如果某个用例修改了 session 级 fixture 的数据会影响后续所有用例。我踩过的坑是把测试用的用户 ID 放在 session fixture 里结果一个用例把用户删了后面所有用例全挂。4.2 参数化一套逻辑跑多组数据pytest 的pytest.mark.parametrize是数据驱动的核心。同一个接口用不同参数跑多遍代码只写一次import pytest pytest.mark.parametrize(user, pwd, expected_code, [ (admin, 123456, 0), (admin, wrong_pwd, 1001), (, 123456, 1002), (not_exist_user, 123456, 1003), ]) def test_login_params(user, pwd, expected_code): resp RequestUtil().send(post, /login, json{user: user, pwd: pwd}) AssertUtil.assert_json_value(resp, code, expected_code)四组数据一个用例函数跑出四条测试结果。Allure 报告里会分别展示每条数据的执行情况哪组数据挂了看得清清楚楚。参数化的数据来源可以更灵活。数据量大的时候从 YAML 或 Excel 读import yaml def load_login_data(): with open(config/test_data/login.yaml, encodingutf-8) as f: return yaml.safe_load(f) pytest.mark.parametrize(case, load_login_data()) def test_login_from_yaml(case): resp RequestUtil().send(post, /login, jsoncase[payload]) AssertUtil.assert_json_value(resp, code, case[expected])这样测试数据和测试代码彻底分离产品经理改测试数据不用碰代码测试人员加用例不用改函数。4.3 用例分层冒烟、回归、全量不是所有用例都要每次都跑。我一般把用例分成三层冒烟用例核心接口的 happy path20 条以内每次提交代码都跑2 分钟内出结果回归用例覆盖主要业务分支200 条左右每天定时跑一次全量用例包含边界值、异常场景上千条发版前跑一次。用 pytest 的 mark 机制来区分pytest.mark.smoke def test_login_success(): ... pytest.mark.regression def test_login_wrong_pwd(): ...跑的时候指定 markpytest -m smoke pytest -m smoke or regression在pytest.ini里注册一下 mark避免 pytest 报警告markers smoke: 冒烟用例 regression: 回归用例4.4 并发执行与用例隔离用例多了之后串行跑太慢。pytest-xdist可以多进程并发pytest -n 44 个进程同时跑理论上速度提升 4 倍。但并发有个前提用例之间必须相互独立。如果用例 A 创建的数据被用例 B 依赖并发执行时顺序就乱了。我处理依赖的方式是每个用例自己准备数据、自己清理数据不依赖其他用例的执行结果。比如测试查询订单接口不要依赖创建订单用例先跑而是在 fixture 里自己创建一个订单用完删掉。pytest.fixture def temp_order(): req RequestUtil() resp req.send(post, /order/create, json{product_id: 1, count: 1}) order_id resp.json()[data][order_id] yield order_id req.send(delete, f/order/{order_id})yield之前是准备yield之后是清理。这样每个用例拿到的都是干净的数据并发也不会互相干扰。5. Allure 报告定制与可视化5.1 Allure 装饰器让报告会说话Allure 报告默认只展示用例名和结果信息量太少。加上装饰器之后报告能展示用例的层级、步骤、严重程度、描述可读性完全不一样。import allure allure.epic(用户中心) allure.feature(登录模块) allure.story(账号密码登录) allure.title(正常登录-返回token) allure.severity(allure.severity_level.CRITICAL) def test_login_success(): with allure.step(步骤1: 发送登录请求): resp RequestUtil().send(post, /login, json{user: admin, pwd: 123456}) with allure.step(步骤2: 校验状态码): AssertUtil.assert_status_code(resp) with allure.step(步骤3: 校验返回token): AssertUtil.assert_json_contains(resp, token)这几个装饰器的层级关系是epic feature story title。报告里会按这个层级组织点开用户中心能看到登录模块再点开能看到具体的用例。allure.step是最实用的它把用例拆成可视化步骤。报告里每一步的执行状态、耗时都清清楚楚失败时能直接定位到是哪一步挂了。5.2 请求响应日志附加到报告光有步骤还不够接口测试最需要的是请求发了什么、响应回了什么。用allure.attach把请求响应内容附加到报告里def attach_request_response(resp, request_data): allure.attach( str(request_data), name请求参数, attachment_typeallure.attachment_type.TEXT ) allure.attach( resp.text, name响应内容, attachment_typeallure.attachment_type.TEXT )更好的做法是把这个逻辑塞进请求封装里每次发请求自动附加。这样用例代码里不用关心报告的事报告里却什么都有。如果响应是 JSON还可以用attachment_typeallure.attachment_type.JSON报告里会格式化展示比纯文本好看得多。5.3 生成与查看 HTML 报告跑完用例后中间结果在reports/allure-results目录里。生成 HTML 报告allure generate reports/allure-results -o reports/html --clean--clean表示先清空输出目录再生成避免旧报告残留。查看报告有两种方式# 方式一直接打开生成的 HTML allure open reports/html # 方式二起一个临时服务查看 allure serve reports/allure-resultsallure serve更方便它会自动起服务并打开浏览器适合本地调试。allure generate生成的静态 HTML 适合归档和分享可以直接打包发给别人。实操心得allure generate生成的报告里如果用了allure serve的临时服务报告里的某些交互功能比如趋势图可能不完整。要完整功能建议用allure generate生成静态报告后用allure open打开。5.4 报告中的失败定位技巧Allure 报告最实用的功能是失败用例的定位。一个用例失败时报告里会展示失败的具体步骤红色高亮失败步骤的请求参数和响应内容如果附加了的话断言失败的期望值和实际值完整的堆栈信息。我排查失败用例的流程是先看失败步骤再看请求参数对不对再看响应内容是不是符合预期最后看断言逻辑有没有写错。90% 的问题在前两步就能定位。如果响应内容太长报告里默认折叠点开就能看到完整内容。配合前面说的日志截断策略报告不会因为响应太大而卡顿。6. 常见问题与排查技巧实录6.1 请求相关的高频问题问题一请求返回 429 Too Many Requests这个错误在接口测试里很常见尤其是并发跑用例的时候。服务端有频率限制短时间内请求太多就被拒了。解决方案有三个层次降低并发数pytest -n 4改成pytest -n 2在请求封装里加退避重试遇到 429 时等待一段时间再重试和开发确认频率限制的具体阈值调整用例执行节奏。退避重试的实现import time def send_with_retry(self, method, path, retry3, **kwargs): for i in range(retry): resp self.send(method, path, **kwargs) if resp.status_code 429: wait 2 ** i logger.warning(f触发频率限制等待 {wait} 秒后重试) time.sleep(wait) continue return resp return resp指数退避2 的 i 次方比固定等待更合理第一次等 1 秒第二次等 2 秒第三次等 4 秒给服务端足够的恢复时间。问题二请求超时但手动调接口正常这种情况通常是环境问题。检查三点base_url 是不是指向了正确的环境、本机网络能不能访问目标服务、服务端是不是有 IP 白名单限制。我遇到过一次用例在本地跑正常在 CI 机器上全部超时最后发现是 CI 机器的出口 IP 不在服务端的白名单里。问题三响应中文乱码requests 默认会根据响应头猜测编码猜错的时候中文就乱码。强制指定编码resp.encoding utf-8或者用resp.content.decode(utf-8)手动解码。6.2 pytest 执行中的典型报错报错一fixture xxx not foundfixture 找不到通常是三个原因fixture 定义在别的文件里但没导入、fixture 名字拼错了、conftest.py 的位置不对。conftest.py 是 pytest 的共享 fixture 文件放在哪个目录就对哪个目录及子目录生效。放在项目根目录全局生效放在 testcases 目录只对 testcases 下的用例生效。报错二用例收集不到pytest 默认只收集test_开头的文件和函数。如果你的文件叫login_test.py或者函数叫check_login()pytest 不会认。要么改名要么在pytest.ini里改python_files和python_functions的规则。报错三断言失败但看不到实际值pytest 的默认断言输出确实不够详细。解决办法是用assert actual expected, f期望 {expected}, 实际 {actual}这种带 message 的写法或者用前面封装的 AssertUtil。6.3 报告生成与展示问题问题一allure 命令找不到说明 Allure 命令行工具没装或者没加到 PATH。检查allure --version能不能输出不能的话重新配置环境变量。问题二报告里没有步骤和附件检查allure-pytest装了没有pytest.ini里--alluredir配了没有。两个都确认了还不行看看用例里有没有加allure.step装饰器。问题三报告打开是空白通常是浏览器兼容性问题。Allure 报告对 Chrome 和 Edge 支持最好用这两个浏览器打开。如果还是空白试试allure serve方式。6.4 常见问题速查表问题现象可能原因解决方向429 Too Many Requests请求频率超限降并发、加退避重试请求超时环境不对/网络不通/白名单检查 base_url 和网络中文乱码编码猜测错误强制 utf-8 编码fixture not found定义位置或名字问题检查 conftest.py 和拼写用例收集不到命名不符合规则改文件名或改配置报告空白浏览器兼容性换 Chrome/Edge断言看不到实际值默认输出太简略加自定义 message并发用例互相干扰用例有依赖用例自备数据、自清理7. 持续集成与框架扩展方向7.1 接入 CI 流水线框架搭好之后最终要落到 CI 里自动跑。核心就三步装依赖、跑用例、归档报告。以常见的流水线配置为例pip install -r requirements.txt pytest -m smoke allure generate reports/allure-results -o reports/html --clean跑完之后把reports/html目录归档为构建产物团队成员随时能下载查看。如果流水线支持展示 HTML 报告直接把报告目录配成报告路径每次构建完自动展示。这样每次提交代码后冒烟用例自动跑报告自动出有问题第一时间发现。7.2 框架的可扩展点这套框架搭好之后还有几个方向可以继续扩展接入数据库校验。接口测试只校验响应是不够的有时候还要校验数据有没有真正落库。用pymysql或SQLAlchemy连数据库在断言里加一层数据校验。接入消息队列校验。有些接口是异步的调用后不直接返回结果而是发消息到队列。这种情况需要消费队列消息来验证。接入 mock 服务。依赖的第三方接口不稳定时用 mock 服务替代保证测试的稳定性。接入测试数据工厂。用Faker库自动生成测试数据避免手工造数据的麻烦。接入性能基线。在断言里加响应时间校验超过阈值就报警把接口测试和性能测试结合起来。7.3 我踩过的几个坑最后分享几个实际踩过的坑都是文档里不会写的坑一token 过期没处理。session 级 fixture 拿到的 token 如果有效期只有 30 分钟跑长用例集时后面的用例会全部 401。解决办法是在请求封装里检测 401自动重新登录刷新 token。坑二测试数据污染。用例创建的数据没清理跑几轮之后数据库里全是垃圾数据查询接口返回的结果越来越长。解决办法是 fixture 里用 yield 做清理或者用独立的测试数据库。坑三断言太严格。比如断言响应时间小于 100ms本地跑没问题CI 机器上因为资源竞争经常超时。断言要留余量响应时间这种指标建议放宽到 2 秒。坑四用例之间有隐藏依赖。单独跑某个用例通过全量跑就失败。这种问题最难查通常是前面的用例改了共享状态。解决办法是每个用例独立准备数据不依赖执行顺序。这套框架我从零搭到稳定运行前后迭代了大概三个月。最开始只有十几个用例后来慢慢加到几百个中间重构过两次目录结构换过一次断言封装。现在回头看最值得投入时间的就是请求封装和 fixture 设计这两块这两块做好了后面加用例就是纯体力活几乎不用动框架代码。
返回列表