资讯动态

Pytest+Requests接口自动化测试框架:从用例组织到持续集成实践

发布时间:2026/9/6 13:09:29 来源:尧图企业网站定制
先说结论Pytest Requests 这套组合做接口自动化测试是目前我认为性价比最高、最容易落地的一种框架形态。它不像 Postman 那样以人工点击为主也不像 JMeter 那样上来就要学习线程组、监听器、断言树更不像 unittest 那样写起来约束多、样板代码重。Pytest 负责组织和执行用例Requests 负责发起 HTTP 请求两者配合可以覆盖绝大多数接口自动化需求登录态处理、参数化用例、批量回归、失败重试、HTML 报告、持续集成。这篇文章适合三类人。第一类已经用 Postman 或 JMeter 做过部分接口测试想换成代码方式维护用例的测试开发第二类刚接触自动化测试知道 Requests 和 Pytest 分别是什么但不知道如何组合成一个完整项目的初学者第三类已经写过一些脚本但想解决用例组织混乱、接口依赖重复、批量任务不稳定等问题的工程实践者。文章会按“设计思路 → 环境准备 → 核心封装 → Pytest 组织 → 依赖与限流 → 报告与排查”的顺序拆解基本就是你从零开始搭建一套框架的完整路径。1. 动手前先搞清楚这套框架到底在替我们解决什么问题很多人一上来就安装库、抄代码结果跑了几天发现脚本变成了一个“能运行但完全没法维护”的文件堆。接口自动化测试最怕的不是接口返回和预期不符而是用例不清晰、重复代码多、跑了出报告也不知道到底覆盖了什么。所以在写任何代码之前先想清楚 Pytest Requests 是在替我们解决什么问题。1.1 手工测试的瓶颈和自动化的目标接口测试本质上就是“发请求、看响应、做断言”三个动作。手工测试也能完成但成本集中在两个地方回归成本高、重复请求多。一个项目一个月迭代两个版本每次发版前都要把几十上百条接口用例重新点一遍点完后还要人工核对响应字段。这种情况下自动化最有价值的切入点不是“替人做测试”而是“把重复的回归动作变成一条命令”。我一直强调一个观点接口自动化测试的目标不是追求用例数量而是追求“稳定覆盖核心链路”。登录态怎么处理、用例之间数据怎么传递、失败后如何定位这些才是框架的核心价值。Requests 负责把请求发出去并取回响应Pytest 负责把这些请求组织成可筛选、可重复执行、可产出报告的用例集。两者结合解决的问题是“如何高效稳定地完成接口回归”。1.2 为什么不用 unittest、Postman 或 JMeterPytest 不是唯一选择但它有几个特点非常适合接口测试。第一是断言简洁。unittest 写一个断言要用self.assertEqual(...)Pytest 直接用 Python 原生的assert就能完成写法少、可读性高报错信息也足够详细。第二是 fixture 机制。接口测试经常需要“先登录、后查询”这种前置操作Pytest 的 fixture 能把这类前置逻辑抽成公共方法不用在每个用例里重复写一遍登录代码。unittest 的 setUp 也能做类似事情但作用域和依赖传递远没有 fixture 灵活。第三是插件生态。Pytest 有参数化、标记、Hook、Allure 报告支持还有超时、重试、顺序控制等插件。这些功能对接口自动化来说几乎都是刚需。JMeter 虽然也能做接口自动化但它更偏向压测脚本化能力、断言可读性、与 CI 的集成体验都要弱一些。Postman 适合调试、适合快速验证但用例管理能力偏弱做到数据驱动、失败重试、多环境切换时明显不如代码框架灵活。1.3 一个框架需要具备的四块拼图不管团队规模大小一套可落到实处的 Pytest Requests 框架至少要包含四块内容用例层每一条测试用例只关注“某个接口在某个条件下应该返回什么”不关心底层 HTTP 连接细节。封装层把请求发送、请求头构造、超时设置、日志打印、响应解析统一封装成一个工具模块避免每个用例直接调用requests.get()并重复处理异常。配置层环境地址、请求超时、账号信息、数据库连接等可变化的配置统一放在配置文件或环境变量中不要散落在用例代码里。报告与执行层通过 Pytest 插件生成可读的测试报告支持按标签过滤用例、支持失败重试、支持 CI 集成。这四块缺一都容易出问题。缺了封装层用例会越来越冗余缺了配置层换环境时改代码改到崩溃缺了报告层任务跑完不知道夜里发生了什么。下面从第一块开始落地。2. 环境准备和项目结构规划进入实际操作之前先准备好开发环境。接口自动化测试项目对硬件要求很低普通办公电脑就可以。建议使用 Python 3.8 及以上版本Windows、macOS、Linux 都可以。下面按搭建流程逐步展开。2.1 Python 和虚拟环境我建议先创建虚拟环境不要一上来就pip install到系统 Python 里。原因很简单不同项目依赖的库版本可能冲突虚拟环境可以把每个项目的依赖隔离开也方便生成requirements.txt让别人一键复现环境。创建虚拟环境的命令python -m venv venvWindows 下激活venv\Scripts\activatemacOS 或 Linux 下激活source venv/bin/activate激活后命令行前面会出现(venv)标识。这时候安装依赖只会装进当前项目的虚拟环境里不会污染全局环境。2.2 安装依赖本项目核心依赖只有三个pip install requests pytest如果需要生成测试报告再装一个pip install pytest-html如果团队喜欢用 Allure 那种更美观、更方便分类的报告可以安装pip install allure-pytest注意我这里没有给具体版本号因为 Requests 和 Pytest 的版本迭代很快安装最新稳定版本即可或者在团队内部统一好版本号再写入requirements.txt。锁定版本的一个重要原因是保证 CI 上执行结果和本地一致避免“本地能跑、流水线挂了”这类问题出现。安装完可以做一次快速验证python -c import requests; import pytest; print(ok)如果输出ok说明环境没问题。2.3 项目目录结构很多初学者喜欢把所有脚本放在同一个目录下比如test_login.py、test_order.py、config.py全部堆在一起。前期用例少还能忍一旦用例超过 50 条修改、定位、维护都会很痛苦。这里给一个建议的目录结构不需要照搬但思路值得参考api_test_framework/ ├── config/ │ ├── __init__.py │ └── settings.py ├── common/ │ ├── __init__.py │ ├── http_client.py │ └── log_util.py ├── testcases/ │ ├── __init__.py │ ├── conftest.py │ ├── test_login.py │ └── test_order.py ├── reports/ ├── logs/ ├── requirements.txt └── pytest.ini从职责上看config/放环境配置比如BASE_URL、超时时间、业务账号。common/放公共模块比如封装后的 HTTP 客户端、日志工具。testcases/放测试用例按业务模块拆分成不同文件。reports/放测试报告。logs/放运行日志。pytest.ini放 Pytest 的配置项。目录为什么要分这么细因为接口自动化项目最后一定会走向“多人协作 多套环境”。如果不提前分好边界后面每个人写代码的习惯都不一样项目会迅速失控。2.4 一个最小用例先跑通不管目标多复杂第一步永远是先跑通一个最小用例。在testcases目录下新建一个test_smoke.py内容如下import requests def test_baidu_status(): resp requests.get(https://www.baidu.com, timeout10) assert resp.status_code 200这只是一个验证环境是否可用的例子实际测试中不要用公网网页当成业务接口来测。跑一下cd testcases pytest test_smoke.py -v如果看到1 passed说明 Pytest 能正常发现用例并执行。这是整个框架的第一个里程碑后面所有功能都基于这个最小闭环扩展。3. 核心封装Requests 底层代码怎么抽才不冗余很多人写接口自动化时最容易犯的一个错误是每个用例里都直接调用requests.get()然后重复写请求头、异常处理、日志打印。比如 10 个用例里有 10 份几乎一样的代码。这种方式在用例数量少时看不出问题用例一多改一个公共请求头就要全局搜索替换。3.1 封装一个 HttpClient 类Requests 本身已经很好用我们封装的目的是减少重复动作、统一处理方式而不是把它变复杂。一个基础版封装至少需要具备以下几点统一base_url统一请求头统一超时时间统一日志记录统一异常处理下面给出一个精简版示例放在common/http_client.pyimport requests import logging class HttpClient: def __init__(self, base_url, timeout10): self.base_url base_url self.timeout timeout self.session requests.Session() self.logger logging.getLogger(__name__) def set_headers(self, headers): self.session.headers.update(headers) def request(self, method, path, **kwargs): url self.base_url path kwargs.setdefault(timeout, self.timeout) self.logger.info(fRequest: {method} {url}) try: response self.session.request(method, url, **kwargs) self.logger.info(fResponse: {response.status_code}) return response except requests.RequestException as e: self.logger.error(fRequest failed: {e}) raise def get(self, path, **kwargs): return self.request(GET, path, **kwargs) def post(self, path, **kwargs): return self.request(POST, path, **kwargs)使用方式就简单多了client HttpClient(http://127.0.0.1:8000) resp client.post(/api/login, json{username: admin, password: 123456})这个类看起来简单但已经把 base_url、timeout、日志、异常都统一掉了。后面如果要在请求头里加一个Authorizationtoken只需要调用set_headers方法即可不需要每个用例都改。3.2 为什么用 Session 而不是直接调用 requests.get代码里我用了requests.Session()这是接口自动化中非常关键的一个细节。Session 对象会自动保持某些连接状态尤其是 Cookie。很多系统登录之后后续接口依赖 Cookie 或 Token 来识别身份。如果每次都直接调用requests.get()Cookie 不会自动保留你就需要手动拿返回值再传给下一个请求。Session 相当于一个带状态的浏览器会话。登录成功后Session 里保存的 Cookie 在后续请求中会自动携带不需要反复赋值。Token 类鉴权可以通过请求头统一注入也可以借助 Session 的 headers。3.3 登录态与 Token 处理绝大多数业务系统的接口都要求先登录。登录成功后会返回 Token 或设置 Cookie。处理方式取决于项目后端设计。如果是 Cookie 方式使用 Session 即可自动维持登录状态不用写额外逻辑。如果是 Token 方式需要在登录接口返回后取出 Token再通过set_headers注入到后续请求中。例如resp client.post(/api/login, json{username: admin, password: 123456}) token resp.json()[data][token] client.set_headers({Authorization: fBearer {token}})实际项目中我会把登录逻辑抽成一个独立方法返回已经带好鉴权信息的 HttpClient 实例。这样不同测试模块拿到的都是一个“已经登录成功”的客户端后面的用例只需要专注业务断言。4. 用 Pytest 组织用例fixture、conftest、参数化、标记Requests 封装好之后接下来要看 Pytest 如何把这些请求过程组织成规范化的测试用例。这一部分决定了项目能否长期扩展。4.1 fixture 和 conftest.py 的作用测试用例之间经常有前置逻辑最典型的就是登录。如果每个用例文件里都写一遍登录代码既不优雅也不利于复用。Pytest 的 fixture 可以解决这个问题。在testcases/conftest.py中定义一个clientfixtureimport pytest from common.http_client import HttpClient pytest.fixture(scopesession) def client(): client HttpClient(http://127.0.0.1:8000) resp client.post(/api/login, json{username: admin, password: 123456}) token resp.json()[data][token] client.set_headers({Authorization: fBearer {token}}) return client然后测试用例文件直接接收这个 fixture 作为参数def test_query_order(client): resp client.get(/api/order/1001) assert resp.status_code 200conftest.py是 Pytest 的特殊文件它里面的 fixture 对同目录及其子目录下的测试用例都可见不需要通过 import 引入。scopesession表示整个测试会话只执行一次登录后续所有用例共用同一个 client这是接口回归中最常见的做法。为什么不用功能函数直接调用因为 fixture 的优势在于作用域控制、自动销毁、依赖注入尤其适合“一个客户端被多个用例复用”的场景。直接调用虽然也能实现但代码会显得散乱且无法利用 Pytest 的缓存机制。4.2 参数化同一接口多组数据接口测试的另一个常见需求是同一个接口输入多组数据验证不同场景。比如一个查询接口需要验证订单号存在、不存在、参数缺失、权限不足等情况。最笨的写法是复制多个用例但这样会造成代码膨胀。Pytest 提供了pytest.mark.parametrizeimport pytest pytest.mark.parametrize(order_id, expected_code, [ (1001, 200), (9999, 404), (, 400), ]) def test_query_order(client, order_id, expected_code): resp client.get(f/api/order/{order_id}) assert resp.status_code expected_code这样一组参数就会生成 3 个独立的测试用例。执行结果中会明确显示是哪一组参数失败了定位非常方便。这里要注意一个原则不要把所有异常场景都堆在参数化里。如果断言逻辑差异很大比如一个要检查状态码、一个要检查数据库落库结果那还是拆成不同用例更清晰。参数化适合“逻辑相同、数据不同”的场景。4.3 标记和过滤冒烟、回归怎么分开项目用例多了之后不是每次都要跑完所有的用例。发布前可能只需要跑冒烟用例全量回归放在夜间执行。Pytest 的标记机制正好解决这个问题。在pytest.ini中注册自定义标记[pytest] markers smoke: 冒烟测试 regress: 回归测试用例中打标记import pytest pytest.mark.smoke def test_login_success(client): resp client.post(/api/login, json{username: admin, password: 123456}) assert resp.status_code 200执行时只跑冒烟用例pytest -m smoke跳过冒烟用例、只跑其他用例pytest -m not smoke这样做的价值是用例数量变大后可以按业务模块、按测试级别、按执行频率打不同标记执行策略从“一次全跑”变成“按需选择”效率和稳定性都会好很多。5. 批量请求、接口依赖与高频请求限流处理真实业务场景里接口很少是孤立的。创建订单后要查询订单登录后才能获取用户信息上传文件后才能拿文件 ID。如何处理这种接口之间的依赖以及批量运行时如何避免请求过快被服务端限制是框架从“能跑”走向“稳定跑”的关键。5.1 接口依赖把返回数据传给下一个用例接口依赖最常见的处理方式是“分层 fixture”。比如创建订单返回订单号查询订单用例依赖这个订单号。在conftest.py中定义创建订单的 fixtureimport pytest pytest.fixture(scopemodule) def created_order_id(client): resp client.post(/api/order, json{product_id: A001, amount: 1}) assert resp.status_code 200 return resp.json()[data][order_id]测试用例接收这个 fixturedef test_query_created_order(client, created_order_id): resp client.get(f/api/order/{created_order_id}) assert resp.status_code 200这样做的好处是创建订单的前置逻辑只写一次多个用例可以共享同一个订单数据并且 Pytest 会严格控制 fixture 的执行顺序先创建订单再执行用例。用例失败后fixture 的执行结果不会残留到其他用例上因为测试之间的数据是隔离的这比很多手工设计的全局变量靠谱得多。5.2 数据传递不推荐用模块级全局变量接口自动化测试中最需要避免的就是“为了省事把接口返回值存到全局变量”。全局变量的问题在于很难追踪这个变量是在哪个用例里被赋值的它会被哪些用例修改并发执行时会不会互相踩踏我的建议是如果一个数据只在一个用例文件内被依赖用模块级 fixture。如果一个数据要被多个用例文件依赖写在更上层的conftest.py中。如果一个数据是请求构造临时用的直接通过参数化传入。最近看一些项目在写依赖时会直接“从上一个用例的返回值里取”看起来省事但用例之间其实产生了顺序耦合。如果某一天只单独跑第二个用例它可能因为缺少前置数据直接失败。使用 fixture 可以在单独执行时也会自动先执行前置逻辑这是更稳妥的做法。5.3 高频请求与 429 限流批量跑用例时一个很容易撞到的坑就是服务端限流。尤其是定时任务、夜间回归、大数据量参数化这三类场景经常出现这样的报错exceeded retry limit, last status: 429 too many requests too many concurrent requests这类报错的含义是客户端在单位时间内发起了过多请求服务端主动拒绝。很多人第一反应是“换 IP”“绕过限制”这绝对不可取。正常情况下应优先从客户端调整请求频率。处理思路有三个层面第一降低并发。如果框架是pytest-xdist多进程执行可以把-n并发数调小比如从 4 改成 2或者干脆顺序执行。第二加入重试机制和退避策略。遇到 429 时不要立即重试等待 1 到 2 秒后再试。可以自己写一个简单的重试装饰器或者使用第三方库。重试逻辑的关键是必须有最大重试次数避免无限循环。第三设置合理超时时间。很多接口慢不是因为业务处理慢而是服务端已经响应不过来客户端还在傻等。设置timeout可以让请求快速失败释放连接。下面是一个最简单的带退避重试的封装思路import time import requests def request_with_retry(session, method, url, max_retries3, **kwargs): for attempt in range(max_retries): response session.request(method, url, **kwargs) if response.status_code 429: time.sleep(2 * (attempt 1)) continue return response raise RuntimeError(exceeded retry limit, last status: 429 too many requests)这段代码很简单但已经能覆盖大部分限流场景。生产级项目还要考虑日志打印每次重试都记录一个 warn方便事后排查。5.4 超时、批量与运行时间如何判断接口自动化框架写完后如何判断它“好不好”我一般看三个维度稳定性连续执行 3 次成功率是否都接近 100%。如果有偶发抖动优先考虑请求超时设置和测试数据冲突。运行时间100 条用例在普通配置下一般应该在 5 到 10 分钟内跑完。如果太慢看是不是接口本身响应慢还是框架里有不必要的time.sleep()。如果太快要警惕是不是没有真实发出请求而是用了 Mock 数据。可排查性任务失败后能否快速看到是哪一个接口、哪一组参数、断言差异在哪。如果日志里只有Requests failed这样的描述那这个框架还不合格。批量任务里最容易忽视的是输出命名和日志切片。多个用例同时跑、多个线程写同一个日志文件会导致日志混乱。建议按用例名、时间戳生成独立日志或者使用 Pytest Hook 把每个用例的执行状态写入结构化日志。6. 测试报告与日常回归的配套集成最后一步也是让整个框架真正可用的一步生成让人看得懂的测试报告并把自动化任务嵌入日常回归流程。6.1 报告选型pytest-html 还是 Allurepytest-html 的优点是安装简单、配置少执行完直接生成一个 HTML 文件适合小团队和快速上线。Allure 的优点是报告更丰富可以展示用例步骤、请求响应、失败截图适合需要精细分析、多人协作的团队。这里以 pytest-html 为例一条命令就能生成报告pytest --htmlreport.html --self-contained-html使用 Allure 时需要先生成结果文件再转换成 HTML 报告pytest --alluredir./allure-results allure generate ./allure-results -o ./reports/ --clean我更推荐的思路是不要一开始就上 Allure那样还需要学习报告服务器的部署。先在 pytest-html 基础上跑起来确认框架能稳定输出结果再根据团队需要切换报告方案。接口自动化的核心是执行逻辑报告只是让结果更清晰。6.2 断言粒度不止是检查状态码很多接口测试的问题在于断言过于宽松。比如只检查status_code 200但后台实际上返回了业务错误。正确做法要分层次状态码层HTTP 状态码是否为 200、400、401。业务码层响应 JSON 里的code字段是否符合预期。数据层关键字段值是否正确比如订单号、金额、用户名。耗时层响应时间是否在可接受范围内。结构层返回 JSON 是否包含预期字段。举一个相对完整的断言示例assert resp.status_code 200 payload resp.json() assert payload[success] is True assert payload[data][order_id] is not None assert resp.elapsed.total_seconds() 3这种粒度会让失败定位变得清晰。否则线上报一个200你以为通过了其实是业务断言逻辑根本不到位这是接口自动化最容易出现的虚假安全感。6.3 常见报错排查链路接口自动化项目实际运行中有几个问题出现频率非常高。这里按排查顺序整理成表格现象优先排查方向处理思路ModuleNotFoundError依赖是否安装激活虚拟环境后重新pip install -r requirements.txt请求超时接口响应慢 or 网络不通先手动请求一次确认接口可用再调大timeout登录失败环境配置错误 or 账号过期检查config中的BASE_URL和账号信息检查 Token 是否过期429 too many requests请求频率太高降低并发数加重试退避避免用全局单 session 高频请求编码乱码响应编码不匹配指定resp.encoding或处理响应头里的 charset用例间数据干扰依赖关系写在用例内部改用 fixture 传递依赖数据断言通过但业务实际失败断言太宽松增加业务码和字段值断言排查时记住一个顺序先看现象再看输入再查环境再改参数。很多人一看到 429 就认为自己被限制其实可能只是测试用例里写了一个循环没控制频率属于客户端自己制造的限流。6.4 什么情况下不要硬套这套框架最后保留一部分边界意识。Pytest Requests 适合大多数 HTTP 接口但如果遇到以下情况需要重新评估高度依赖 RPC、Dubbo、Kafka 等非 HTTP 协议Requests 无能为力。项目接口文档不完善、接口定义频繁变动自动化维护成本会很高。团队完全没有编码能力又希望快速产出报告优先用低代码平台可能更合适。用例执行依赖大量本地文件、数据库状态、缓存状态需要先解决测试数据准备方案。另外还要注意千万不要为了追求框架复杂度而过度设计。一个小项目只有 10 条用例非要引入数据库连接、Docker、CI 流水线、多环境配置那是给自己找麻烦。先跑单任务再跑批量先本地执行再集成到流水线节奏更安全。踩过几次之后我发现很多接口自动化项目不是死在工具能力上而是死在两头要么太简陋全部逻辑堆在用例里要么太复杂光框架代码就写了一千多行。Pytest Requests 的价值恰恰在于平衡它给你提供了组织用例和封装请求的基础能力但不会逼你一开始就把架构建得特别庞大。先把单条用例跑稳再把登录和 Token 处理好然后把批量执行和报告补上这套框架就能稳定服务日常回归后面再逐步扩展限流处理、数据依赖和持续集成都是水到渠成的事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价