做接口自动化这几年跟不少同行聊过发现很多人卡在同一个地方不是不会写 requests 脚本而是写出来的脚本只能自己跑、不能沉淀成框架。今天我把这套 Python requests pytest 的接口自动化测试框架搭建过程完整写下来从环境搭建、用例设计、数据驱动到 Allure 报告、CI 集成每一步都给出我在实际项目中用过的方案和踩过的坑。适合刚接触接口自动化的测试新人也适合已经写了一堆脚本但想往框架方向整合的中级测试开发。1. 框架整体设计与目录规划1.1 框架选型的核心思路先聊为什么选 Python requests pytest而不是 Java TestNG 或者 Python unittest。接口自动化的本质就是发 HTTP 请求、校验响应、生成可读的报告。Python 在这件事上写得最快requests 库是事实上的 HTTP 客户端标准几乎每个搞过接口测试的都用过。pytest 比 unittest 强在三点断言更简洁原生assert就行、fixture 机制灵活、插件生态丰富尤其是 Allure 报告插件。我做过的几个项目从 unittest 迁移到 pytest工作量并不大但写用例的体验差很多。unittest 的 setUP/tearDown 是类级别的强制约定pytest 的 fixture 则是按需注入两者设计哲学就不同。选 pytest 的核心理由还有一个数据驱动。接口测试特别是 To B 系统同样的接口几十种参数组合pytest 的参数化parametrize配合外部数据文件可以直接把用例数和数据量解耦。1.2 目录结构与模块划分框架的目录结构我建议一开始就定好不然后面维护成本几何级增长。下面是我在多个项目中沉淀下来的模板api_auto_framework/ ├── config/ │ ├── __init__.py │ ├── settings.py # 全局配置环境地址、超时、密钥等 │ └── test_data/ │ ├── login_cases.json │ └── order_cases.yaml ├── common/ │ ├── __init__.py │ ├── request_client.py # requests 封装 │ ├── assert_utils.py # 断言工具 │ ├── log_utils.py # 日志收集模块 │ └── db_utils.py # 数据库校验工具可选 ├── testcases/ │ ├── __init__.py │ ├── conftest.py # fixture 汇总 │ ├── test_login.py │ └── test_order.py ├── test_data/ │ └── login_cases.yaml ├── reports/ │ ├── allure-results/ │ └── logs/ ├── pytest.ini └── requirements.txt这个结构的核心思路是配置、数据、用例、工具四层分离。测试数据不写在代码里环境地址不写死在脚本里公共操作抽到 common 目录。刚开始可能觉得多此一举但当用例量到五百条以上、环境从测试切到预发、接口字段变更时你就知道这套分层的价值了。2. 环境搭建与核心依赖配置2.1 Python 环境与虚拟环境管理先啰嗦一句版本问题。Python 版本建议 3.9 以上requests 和 pytest 的生态对 2.x 已经完全不支持了。虚拟环境我用 venv不用 conda除非你同时搞数据分析。venv 的好处是零依赖、不污染全局环境而且现在 PyCharm 和 VS Code 对 venv 的支持都很成熟。创建虚拟环境并激活python -m venv venv source venv/bin/activate # Windows 是 venv\Scripts\activate看到命令行前面多了(venv)前缀就对了。这一步的核心价值是隔离项目依赖。我见过一个真实事故同一个机器上两个自动化项目都依赖 requests一个项目升级 requests 到新版后另一个项目跑挂。虚拟环境从根上解决这个问题。2.2 核心依赖安装requirements.txt 里我建议至少包含requests2.31.0 pytest8.2.0 pytest-html4.1.0 pytest-assume2.9.1 allure-pytest2.13.5 pyyaml6.0 openpyxl3.1.2安装命令pip install -r requirements.txt这里说两句为什么指定版本。requests 2.31 以上修复过多个已知安全问题pytest 8.x 对参数化的性能优化很明显。allure-pytest 版本必须和 Allure 命令行工具的版本匹配否则跑了半天报告生成不出来。2.3 pytest 配置文件详解pytest 的配置我放在 pytest.ini 而不是 pyproject.toml原因很简单pytest 对 pytest.ini 的读取优先级最高不容易被其他工具干扰。一个典型的配置[pytest] testpaths testcases addopts -v -s --alluredirreports/allure-results --maxfail2 markers smoke: 冒烟测试 regression: 回归测试 p0: P0级用例 log_cli true log_cli_level INFOtestpaths指定 pytest 去哪里找用例addopts是每次执行默认带的参数。--maxfail2的意思是遇到 2 个失败就停止这在调试阶段非常实用不然几十个用例一直跑看日志看到眼睛花。markers 是标签系统后面我们用-m smoke只跑冒烟用例。3. 测试用例设计与编写实战3.1 接口用例的通用结构与断言写法接口用例写多了你会发现大部分用例骨架是一样的准备数据、发请求、校验响应。所以封装一个请求客户端是第一步。我封装 requests 的核心原因有两个统一处理 base_url、统一处理 token 和日志记录。# common/request_client.py import requests import json import logging from config.settings import BASE_URL, DEFAULT_TIMEOUT, SECRET_KEY logger logging.getLogger(__name__) class RequestClient: def __init__(self): self.base_url BASE_URL self.session requests.Session() self.logger logger def request(self, method, url, **kwargs): url self.base_url url kwargs.setdefault(timeout, DEFAULT_TIMEOUT) # 记录请求参数便于排查问题 self.logger.info(f请求: {method} {url} 参数: {json.dumps(kwargs, ensure_asciiFalse)}) response self.session.request(method, url, **kwargs) self.logger.info(f响应: 状态码 {response.status_code} 内容: {response.text[:200]}) return response def get(self, url, **kwargs): return self.request(GET, url, **kwargs) def post(self, url, **kwargs): return self.request(POST, url, **kwargs)有了这个封装用例就变得很干净。比如测试登录接口# testcases/test_login.py import pytest from common.request_client import RequestClient client RequestClient() def test_login_success(): resp client.post(/api/login, json{username: testuser, password: 123456}) assert resp.status_code 200 data resp.json() assert data[code] 0 assert data[data][token] ! 这里用到了 pytest 最舒服的原生断言。不需要assertEqual、assertIsNotNone这类冗长写法直接assert就行。失败时 pytest 会打出具体参数差异定位问题极快。3.2 fixture 的用法与 conftest.py 的威力fixture 是 pytest 的灵魂也是最容易被新手上手时忽略的部分。fixture 本质是一个可复用的初始化/清理逻辑。登录接口的 token 几乎每个用例都要用你可以选择每个用例里都调一次登录——劣化明显或者写一个 session token 的 fixture只在模块内共享。# testcases/conftest.py import pytest from common.request_client import RequestClient pytest.fixture(scopesession, autouseTrue) def global_setup(): 全局初始化确保测试环境可用 resp RequestClient().get(/api/health) assert resp.status_code 200, 测试环境不可用请检查服务状态 yield pytest.fixture(scopesession) def auth_token(): 登录一次整个测试会话复用 token resp RequestClient().post(/api/login, json{username: admin, password: admin123}) assert resp.status_code 200 return resp.json()[data][token]scopesession表示整个测试会话只执行一次autouseTrue表示每个用例自动执行。这样全局环境检查只需要做一次。yield后面可以写清理逻辑比如登出、删除创建的数据是 teardown 的替代品。3.3 断言进阶与预期结果管理单个assert只能判断一个条件但接口返回经常要同时校验多个字段。逐条写assert会啰嗦用 pytest-assume 可以把断言变成软断言即一个断言挂掉后后续断言继续执行最后统一汇总失败import pytest pytest.mark.parametrize(case_data, load_cases()) def test_order_query(case_data, auth_token): resp RequestClient().post(/api/order/query, jsoncase_data[request], headers{Authorization: fBearer {auth_token}}) data resp.json() with pytest.assume: assert resp.status_code case_data[expected][status_code] assert data[code] case_data[expected][code] if order_id in case_data[expected]: assert data[data][order_id] case_data[expected][order_id]pytest.assume的with写法是 2.9 以后新增的比早期的逐行pytest.assume(condition)更易读。遇到响应数据结构复杂的情况建议先resp.json()转字典再用assert data[data][field] expected逐项比较。4. 数据驱动与 Mock 扩展4.1 数据驱动实战从 YAML 读取用例接口自动化的重头戏是数据驱动。什么意思把测试数据从代码里剥离出来放到外部的 YAML/JSON/Excel 文件里一条用例就是一个数据条目。代码作为执行引擎数据决定跑什么、断言什么。我推荐 YAML 格式原因是 YAML 的缩进结构对嵌套数据表达非常友好注释也方便。一个典型的用例文件# test_data/login_cases.yaml login_success: description: 正常登录 request: method: POST url: /api/login json: username: admin password: admin123 expected: status_code: 200 code: 0 token_not_empty: true login_invalid_password: description: 密码错误 request: method: POST url: /api/login json: username: admin password: wrong123 expected: status_code: 200 code: 1001 message: 密码错误然后加载数据并参数化import pytest import yaml def load_cases(file_path): with open(file_path, encodingutf-8) as f: return yaml.safe_load(f) class TestLogin: pytest.mark.parametrize(key,case_data, load_cases(test_data/login_cases.yaml).items()) def test_login(self, key, case_data): resp RequestClient().request( case_data[request][method], case_data[request][url], jsoncase_data[request].get(json, {}) ) resp_data resp.json() assert resp.status_code case_data[expected][status_code] assert resp_data[code] case_data[expected][code] if case_data[expected].get(token_not_empty): assert resp_data[data][token] ! 这一下就把用例从代码里解放出来了。业务新增一个用例只需要在 YAML 里加一段数据不需要改代码。我在某项目中用这套方案把 200 多条接口用例浓缩成 30 多个 YAML 片段维护成本下降了至少一半。4.2 Mock 数据的引入Mock 是接口测试里绕不开的话题。第三方服务不稳定、支付渠道没有测试环境、某个上游接口还没开发完这些场景都需要 Mock。在 pytest 生态里 mock 最轻量的方式是 pytest-mock它是 unittest.mock 的 pytest 封装。比如我们测试订单接口依赖一个外部物流查询服务实际测试环境没有这个服务def test_create_order_with_mock(mocker): # mock 掉外部物流服务避免真实请求 mocker.patch(common.request_client.RequestClient.post, return_valueSimpleNamespace(status_code200, jsonlambda: {code: 0, data: {delivery_no: MOCK123}})) resp RequestClient().post(/api/order/create, json{items: [{sku: A001, qty: 1}]}) assert resp.status_code 200注意Mock 要控住作用域。mocker.patch默认在单个测试函数结束后自动恢复不会影响其他用例。这里给一个忠告Mock 是为了绕过不可控依赖不是为了假造通过。你要明确 Mock 的是什么、Mock 之后的断言逻辑是否仍然真实。我见过团队把核心数据库查询也 Mock 掉结果接口实际调用时挂了一堆后来排查才发现 Mock 过多掩盖了真实问题。4.3 日志收集体系的搭建日志是排查接口测试问题的第一利器。requests 和 pytest 默认的 print 输出太杂建议用 logging 模块统一。在 conftest.py 里配置全局日志格式带时间、文件名、行号import logging def setup_logging(): formatter logging.Formatter( %(asctime)s [%(levelname)s] %(name)s:%(lineno)d | %(message)s ) handler logging.StreamHandler() handler.setFormatter(formatter) root_logger logging.getLogger() root_logger.setLevel(logging.INFO) root_logger.addHandler(handler) setup_logging()在实际项目中日志分几层请求日志由 RequestClient 输出、断言日志记录预期 vs 实际、系统日志pytest 集成的打印信息。加一个按日期滚动的文件 handler 也很简单这样每次回归跑完日志文件可以直接归档排查线上问题时有据可查。5. Allure 报告与执行策略5.1 Allure 报告接入Allure 是我做过所有测试报告方案里最值得推荐的一个。它最大的价值不只是显示通过/失败而是把用例的步骤、附件、参数、断言结果都结构化呈现。接入分两步安装命令行工具 装 pytest 插件。命令行工具在 Linux 下用 apt/brew 装Windows 下需要下载解压包并加到 PATH。装好之后pytest --alluredirreports/allure-results allure generate reports/allure-results -o reports/allure-report --clean allure open reports/allure-report这里说下--alluredir指定的是原始结果目录generate 的时候会读取这些结果生成静态 HTML 报告。我遇到过很多人直接allure open allure-results报错原因就是把结果目录和报告目录搞混了。为了用例可读性建议在用例里加上 Allure 的描述标记import allure allure.feature(登录模块) allure.story(正常登录场景) allure.title(使用正确密码登录成功) allure.severity(allure.severity_level.CRITICAL) def test_login_success(): ...这样报告打开就是分层展示Feature - Story - 具体用例非常适合跟产品和开发同步回归结论。5.2 重试机制与超时控制接口测试最恼人的问题是偶发性失败。网络抖动、上游接口响应慢、缓存没刷新都可能导致用例误报。pytest-rerunfailures 插件专门解决这个问题pip install pytest-rerunfailures在 pytest.ini 的 addopts 里加--reruns2 --reruns-delay3表示失败后重跑 2 次每次间隔 3 秒。注意重试只对断言失败的用例生效对 setup/teardownfixture的异常无效。这点要牢记不然你会被奇怪的 retry 行为坑到。超时控制同样重要。一个接口卡死如果没设超时整条用例就挂在那里。在封装 RequestClient 时我把timeout设为(connect_timeout, read_timeout)的元组结构连接超时和读取超时分开控制避免混在一起说不清楚DEFAULT_TIMEOUT (5, 15) # 连接5秒读15秒5.3 执行策略冒烟与全量回归框架搭好后执行策略是效率和稳定性的平衡点。我通常维护三档用例集策略命令适用场景冒烟测试pytest -m smoke --alluredirreports/allure-results每次代码部署后快速验证核心可用性回归测试pytest -m regression --alluredirreports/allure-results提测时跑全量回归单接口调试pytest testcases/test_login.py -v本地开发联调结合 Jenkins 等 CI 工具建议在流水线里区分触发条件提交代码时只跑冒烟每晚定时任务跑回归。这样既保证反馈速度又覆盖关键路径。6. CI 集成与常见问题排查实录6.1 与 CI 集成的关键配置框架跑在 CI 上和本地跑有个微小的差异CI 是没有交互式的环境所以所有路径最好用相对路径测试数据改用环境变量注入。以 CI 平台的通用配置为例流水线里加一个步骤pip install -r requirements.txt pytest --alluredirreports/allure-results --maxfail3 # 然后执行报告生成与归档环境变量的读取我放在 settings.py 里import os BASE_URL os.getenv(TEST_BASE_URL, http://127.0.0.1:8080) SECRET_KEY os.getenv(SECRET_KEY, dev-secret)这样测试环境、预发环境的地址切换不需要改代码文件只需要在 CI 里配置不同的环境变量。注意密钥类的信息千万别硬编码传输时也要走 CI 的机密变量通道。6.2 常见问题与排查技巧实录我在这套框架上踩过的坑列成速查表希望你能避过去症状原因解决方案用例报FixtureNotFoundErrorfixture 定义在非 conftest.py 文件或作用域不匹配fixture 放到 testcases 下的 conftest.py检查scope参数中文乱码requests 未指定encoding响应编码被误判resp.encoding utf-8后再取 textAllure 报告生成为空pytest 插件版本和 allure 命令行不匹配用allure --version和pip show allure-pytest对照版本参数化用例太多导致报告巨长每个参数都是一个独立用例用pytest.param(id描述)设置可读的用例 ID报告展示时更清晰登录 token 在用例间失效session fixture 被误用为 function 级确认 token fixture 的 scope 是session或module每次跑用例都在重复登录大量用例各自请求登录接口全局只定义一个 auth fixture其他用例用参数注入举个具体的排查实例。某次回归测试跑出 20 多个用例失败翻日志发现全部是 500 错误但本地手动调用接口没问题。最后定位到原因CI 环境变量里没有把TEST_BASE_URL指向正确环境导致测试打到了旧服务。从那以后我在 global_setup 这个 autouse fixture 里加了一步健康检查环境不对直接 fail fast剩下的时间不再浪费在无意义的请求上。6.3 从框架搭建到全栈质量保障的思考框架搭到能用只是第一步真正的价值在于围绕框架建起一套质量反馈循环。我目前建议团队里做接口自动化的同学往两个方面延伸一是数据库校验接口写入数据后去数据库查一下落库是否正确二是性能关注点在回归跑批时给关键接口记录响应时间指纹发现响应时间异常飙高时自动告警。数据库校验我用 db_utils 封装一个连接池只读查询走独立的只读账号避免测试数据污染。查询结果和接口响应联动断言能让接口测试从黑盒变成灰盒覆盖面大很多。响应时间指纹则是另一重保障。同样是登录接口功能通过不代表性能没退化。我在断言之后加一个耗时阈值校验import time start time.time() resp client.post(/api/login, json...) elapsed time.time() - start assert elapsed 2.0, f登录接口响应耗时异常: {elapsed:.2f}s这个值如果出现波动通常是数据库慢查询或缓存失效导致。在 CI 上配合历史趋势图表能提前发现系统性能劣化。讲到全栈接口自动化和前端自动化、单元测试其实是可以打通的。pytest 本身能跑单元测试所以接口测试框架里也可以通过 tox 或抹平配置去跑后端的单测用例。团队里一个测试开发如果能把接口回归、日志监控、性能预警串起来项目质量的可视化程度会高很多。7. 框架落地后的经验心得框架搭好只是开始。我在实际使用中最大的感受是四个字持续演进。这套框架在最初版本只有 30 个用例后来增长到 500 多个过程中几乎每周都有小的调整断言工具拆出去了、数据文件换了格式、加了 DB 校验模块。框架一定要保持简单别一开始就追求大而全的插件否则用例没写多少框架本身成了最大的维护负担。最后分享两个小建议。第一用例的命名一定要干净test_login_success比test_01重要得多因为报告的可读性直接取决于用例名称。第二写接口测试用例的时候多想想业务异常路径别只盯着主流程。登录密码错误、参数缺失、非法 token 这些用例才是接口测试真正区别于手工回归的价值所在。这套框架跑起来后你会在某一天发现一次回归从之前手点的两天缩短到半小时而且每次跑出来都是一份结构清晰、证据完整的报告。那种感觉才是接口自动化真正该有的状态。