
做了五六年接口测试脚本从最早的几十行 requests 堆在一起到后来每个项目都有一堆“一次性脚本”维护成本高到让人想辞职。后来老老实实把反复用到的东西抽出来搭了一套可以复用的接口自动化测试框架。这篇文章把整个搭建过程复盘一遍从设计思路到具体代码、从踩坑记录到 CI 集成尽量说清楚每个模块为什么这么设计你就知道从零到一怎么把框架搭起来而不是只拿到一堆概念。如果你正打算从 Postman 手工测试转向自动化回归或者已经在写脚本但感觉维护越来越吃力这篇文章都值得看完。我会以 Python requests pytest allure 这条主流技术栈为主线展开所有代码和配置可以直接抄走改一改就能用。1. 框架整体设计思路先明确要解决什么问题1.1 为什么要自己搭框架而不是继续用工具很多人会问Postman 不是也能做接口测试吗JMeter 还能压测为什么非要自己搭一套框架我的答案是取决于你的测试规模和团队协作方式。如果你只是偶尔测一两个接口Postman 完全够用。但当你需要做接口回归时问题就来了几十个接口、几百条用例、不同环境的 base_url、不同的登录态、每次执行完要出一份让人看得懂的报告、还要接入 CI 在每次发版前自动跑一遍。这时候 Postman 的集合模型就有点吃力了尤其当断言逻辑复杂、需要自定义前置数据处理、需要造数据库数据时工具的表达能力非常有限。而自己搭框架的核心价值不是“写代码比工具高级”而是把测试执行过程变成一组可维护、可扩展、可追溯的资产。用例是文本文件放在 Git 里可以走 review 流程执行结果有标准报告出现失败可以快速定位是接口 bug 还是用例问题。这套东西一旦形成团队里任何人接手都能跑起来不会因为某个人电脑里存了一堆 Postman 集合就寸步难行。网上关于接口自动化测试框架的热搜词很密集像“pytest 框架”“python 做接口自动化测试”“java 接口自动化测试框架”等等。这说明大家都想找一个成熟可靠的落地方案。我在面试中也经常看到候选人简历写着“熟练掌握接口自动化框架”但细问之下很多只是会写 requests 脚本。真正的框架是有一整套设计和工程化支撑的。1.2 技术选型Python requests pytest 这条线为什么最稳技术选型是搭框架第一步也是最容易纠结的一步。我实测比较下来最推荐、也最适合大多数团队起步的组合是语言Python 3.10HTTP 客户端requests测试框架pytest报告allure-pytest数据驱动YAML 文件 pytest 参数化配置管理YAML 环境变量之所以这样选主要有三个原因。第一Python 的语法简单团队里即使是做测试不久的同学也能快速看懂和编写用例。相比 Java RestAssured 的方案Python 在用例可读性和开发效率上优势明显尤其是当你需要写复杂的数据处理逻辑时这个优势会被放大。第二pytest 是当前 Python 生态里功能最完整、社区最活跃的测试框架。它的 fixture 机制非常适合做接口测试中常见的初始化、登录、清理等前置后置工作参数化天然适合做数据驱动插件体系强大allure 报告、重试、超时、并发执行都有成熟的第三方插件。第三requests 简单直接封装成本低。有人觉得 requests 太底层想用 httpx 或者直接封装一套复杂的 HTTP 客户端我建议初期不要过度设计。requests 的 Session 已经帮我们处理了连接复用、cookie 持久化、代理配置等问题足够覆盖绝大多数接口场景。提示如果你所在团队的后端是 Java 技术栈甚至用了若依这类快速开发框架也不需要因此改成 Java 写接口自动化。外部接口测试的语言和技术栈跟后端用什么框架没有必然关系。测试侧快速迭代和稳定运行才是第一位的。1.3 模块划分与目录结构一开始就把边界划清楚框架搭建最忌讳的就是把所有的代码堆在几个大文件里。我见过有的人把请求封装、用例、断言全部写在 test_api.py 里一个文件三千行跑起来能跑但谁都不敢动。我建议按下面的目录结构来组织层与层之间职责分明auto_test/ ├── configs/ │ ├── test.yaml │ └── prod.yaml ├── core/ │ ├── __init__.py │ ├── config.py │ ├── http_client.py │ ├── assertion.py │ ├── logger.py │ └── auth.py ├── testcases/ │ ├── user_cases.yaml │ └── order_cases.yaml ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_user_api.py │ └── test_order_api.py ├── reports/ ├── logs/ ├── requirements.txt ├── pytest.ini └── main.py这个结构体现了三层分离的思路core 是框架核心层封装配置、请求、断言、日志等通用能力testcases 是数据层存放 YAML 格式的用例数据tests 是业务层放具体的 pytest 用例脚本和 fixture。边界清晰之后最大的好处是核心层代码一般只需要写一次业务层的人只需要关注“这个接口怎么测”新增一个接口测试就是加一段 YAML 或加一个 pytest 函数不用去动底层封装。这就是框架能够持续扩展而不至于腐化的关键原因。2. 核心模块逐个拆解骨架与血肉2.1 配置管理环境切换、密钥、超时统一收口框架里的配置管理核心目标只有一个让环境切换变成一件“改一个变量”的事而不是去代码里搜索替换 base_url。我用 YAML 存配置每个环境一个文件。configs/test.yaml 的示例base_url: http://127.0.0.1:8000 timeout: 10 db: host: 127.0.0.1 port: 3306 user: test_user password: 123456 log: level: INFO dir: logs/然后写一个 Config 类来加载和读取配置import os import yaml class Config: def __init__(self, envNone): self.env env or os.getenv(TEST_ENV, test) config_path os.path.join( os.path.dirname(__file__), .., configs, f{self.env}.yaml ) with open(config_path, r, encodingutf-8) as f: self.data yaml.safe_load(f) property def base_url(self): return self.data[base_url] property def timeout(self): return self.data.get(timeout, 10) def get_db_config(self): return self.data.get(db, {})运行测试时通过环境变量 TEST_ENV 指定环境比如在本地执行TEST_ENVtest pytest -v在预发环境执行TEST_ENVstaging pytest -v这样配置和代码彻底分离环境相关的信息不会散落在各个用例里。有同事接手你的框架时只需要打开 configs 目录就能了解所有环境信息不用读代码。2.2 请求封装把“发一次接口”这件事收敛成一行requests 本身很好用但直接用 requests 发请求会有一个问题每个测试里都要写一大堆参数而且日志、超时、异常处理逻辑会重复。所以在 core 里封装一个 HttpClient统一处理这些事情。我的封装思路是一个模块负责完成一次请求的完整生命周期包括记录请求详情、设置默认超时、处理响应内容、判断 HTTP 错误。import requests from core.logger import logger class HttpClient: def __init__(self, base_url, timeout10): self.session requests.Session() self.base_url base_url.rstrip(/) self.timeout timeout self.session.headers.update({Content-Type: application/json}) def request(self, method, url, **kwargs): if not url.startswith(http): url f{self.base_url}/{url.lstrip(/)} kwargs.setdefault(timeout, self.timeout) logger.info(f请求 {method.upper()} {url} params{kwargs.get(params)} json{kwargs.get(json)}) resp self.session.request(method.upper(), url, **kwargs) logger.info(f响应 {resp.status_code} body{resp.text}) return resp def get(self, url, **kwargs): return self.request(get, url, **kwargs) def post(self, url, **kwargs): return self.request(post, url, **kwargs) def put(self, url, **kwargs): return self.request(put, url, **kwargs) def delete(self, url, **kwargs): return self.request(delete, url, **kwargs)这里用到了 requests.Session这是一个很多人没注意到的细节。Session 底层维护了一个连接池复用同一个 TCP 连接性能比每次 new 一个连接好很多而且 cookie 会自动保存。在接口自动化中如果你的用例之间依赖登录状态Session 还能自动把 set-cookie 带来的会话状态保留下来。日志在调试接口时极其重要。你想象一下一个用例失败后如果日志里能看到完整的请求 URL、请求头和响应体你几乎不需要再打开 Postman 手动复现直接就能判断是参数问题还是服务端问题。关于“分层”的度我的建议是不要做过分花哨的封装。比如有的框架会写一个 RPCClient把每个业务接口都定义成一个方法虽然调用起来方便但接口一旦有参数变化你就要改类代码维护成本反而高。我倾向于保持 request/get/post 这种通用层业务参数通过 YAML 和用例函数传入。2.3 数据驱动用例写入 YAML代码负责执行数据驱动是接口自动化框架的核心特性。它的含义是把用例的输入数据和期望结果从代码中剥离出来放到 YAML 文件里代码只负责执行和校验。好处有两点一是用例可读性大大提高产品、测试都能看懂二是新增用例不需要改代码只要按模板写 YAML 就行。我的 YAML 用例模板长这样base: method: GET path: /api/v1/users headers: token: ${token} cases: - id: query_users_success name: 查询用户列表成功 params: page: 1 size: 10 validator: status_code: 200 json: code: 0 message: success - id: query_users_with_empty_size name: 查询用户列表时size为空 params: page: 1 size: validator: status_code: 200 json: code: 10001 message: size不能为空在 pytest 函数中动态读取这些 YAML 文件并参数化执行import pytest import yaml from pathlib import Path from core.http_client import HttpClient def load_yaml_cases(file_name): path Path(__file__).parent.parent / testcases / file_name with open(path, r, encodingutf-8) as f: data yaml.safe_load(f) return data[base], data[cases] pytest.mark.parametrize(case, load_yaml_cases(user_cases.yaml)[1], idslambda c: c[name]) def test_user_api(client, case): resp client.request( case[method], case[path], paramscase.get(params), jsoncase.get(json), headerscase.get(headers), ) assert resp.status_code case[validator][status_code] resp_json resp.json() for key, expected in case[validator][json].items(): assert resp_json.get(key) expected, f字段 {key} 不一致这种模式下业务测试人员完全可以“不碰代码只看 YAML”来新增用例。你只需要告诉他们每个字段的含义他们自己就能扩展用例库。2.4 断言与校验不止是单字段断言很多接口自动化脚本里的断言就一句话assert resp.json()[code] 0。这个写法在用例少的时候没什么问题但一旦用例多了你就发现错误信息不友好、断言能力不够用。我的建议是封装一组统一的断言工具类把常见校验逻辑收口。比如class Assertion: staticmethod def equal(actual, expected, fieldNone): assert actual expected, f字段 {field or 值} 断言失败: expected {expected}, actual {actual} staticmethod def not_none(value, fieldNone): assert value is not None, f字段 {field or 值} 不应该为空 staticmethod def in_list(value, candidates, fieldNone): assert value in candidates, f字段 {field or 值} 的取值 {value} 不在预期范围内 {candidates} staticmethod def match_regex(value, pattern, fieldNone): import re assert re.search(pattern, value), f字段 {field or 值} 匹配正则失败: {value} 不匹配 {pattern} staticmethod def less_than(value, limit, fieldNone): assert value limit, f字段 {field or 值} 超过阈值: {value} {limit}通过这种方式断言失败时错误信息会告诉你具体是哪个字段、期望值是多少、实际值是多少而不是干巴巴的一个 AssertionError。这在跑几百条用例时特别重要否则失败了你还得重新去调试。另外很多接口除了 JSON 字段还需要校验响应时间。接口自动化经常忽略性能指标但实际业务中对接口的响应时间是有要求的。可以在断言工具里加一个响应时间校验把响应耗时作为断言的一部分。3. 从零搭建全过程跟着做一遍3.1 环境准备与初始化在动手写框架之前先把环境准备好。我基于 Python 3.10 运行建议用虚拟环境隔离项目依赖避免污染全局环境。mkdir auto_test cd auto_test python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate然后创建 requirements.txtrequests2.31.0 pytest7.4.0 PyYAML6.0.1 allure-pytest2.13.2安装依赖pip install -r requirements.txt如果你想在本地生成和查看 allure 报告还需要安装 allure 命令行工具。Mac 上可以用 brew install allureWindows 上可以下载 allure 压缩包后配置 PATH 环境变量。3.2 配置模块与请求模块落地按照前面目录结构的规划先写 config.py 和 http_client.py。配置模块的代码前面已经给出了http_client 也给出了但有两个细节必须提醒。第一个细节base_url 拼接时一定要做去末尾斜杠和添加开头斜杠的处理。否则你配置里写http://xxx/api/用例 path 写/v1/users拼出来就是http://xxx/api//v1/users很多后端框架会直接 404排查起来很让人抓狂。第二个细节requests 的 json 和 data 参数区别。json 参数会自动把 dict 序列化成 JSON 字符串并且设置 Content-Type 为 application/jsondata 参数是表单格式。接口开发中两者经常混用所以封装的 request 方法里要留意用 kwargs 接收不要默认强制某一种。3.3 fixture 串联测试生命周期pytest 的 fixture 是框架里最核心的机制。我们用 fixture 来做客户端初始化、登录态获取、用例执行后的清理。conftest.py 示例import pytest from core.config import Config from core.http_client import HttpClient from core.auth import Auth pytest.fixture(scopesession) def config(): return Config() pytest.fixture(scopesession) def client(config): return HttpClient(config.base_url, config.timeout) pytest.fixture(scopesession) def auth_token(client): resp client.post(/api/v1/auth/login, json{username: admin, password: 123456}) assert resp.status_code 200 return resp.json()[data][token]这里 scopesession 表示整个测试会话只执行一次。如果放在函数级别每个用例都会登录一次几百条用例下来时间浪费非常明显。登录这个操作通常只做一次把 token 注入到客户端 header 中即可pytest.fixture(scopesession) def client_with_token(client, auth_token): client.session.headers.update({Authorization: fBearer {auth_token}}) return client以后用例如果需要登录态就在函数参数里加 client_with_tokenpytest 会自动注入。3.4 执行与报告pytest allure所有模块和用例都写完后执行命令如下pytest -v --alluredir reports/allure_results跑完后生成报告allure generate reports/allure_results -o reports/allure_report --clean allure open reports/allure_reportallure 报告的亮点在于它能以时间轴和图表的维度展示用例通过情况、失败原因、步骤日志。如果我们配合 pytest.ini 做一些基础配置体验会更好[pytest] testpaths tests addopts -ra --strict-markers markers smoke: 冒烟用例 regression: 回归用例这样在挑选执行范围时可以按标记执行比如只跑冒烟用例pytest -m smoke4. 常见问题与排查技巧实录4.1 Token 和登录态要如何自动处理接口自动化最常见的拦路虎就是登录态。很多项目现在用 JWT token登录之后拿到一个 token所有接口都要在 header 里带上 Bearer token。最简单粗暴的做法是每个用例脚本自己调登录接口但这样很蠢登录接口会被调用几百次而且一旦登录逻辑报错所有用例都失败排查起来一团糟。正确的做法是用会话级 fixture 拿一次 token然后通过注入 header 的方式自动加到所有请求上。这样每个用例不需要关心登录逻辑只管自己的业务接口即可。如果遇到 token 有效期特别短比如 30 分钟的情况用例执行超过 token 有效期就都会失败。应对方案有两个一是把 token 过期时间在测试环境调长需要跟后端确认二是写一个自动刷新 token 的组件检测到 401 时自动重新登录并重放请求。这个后面在重试部分一起说。4.2 接口依赖和数据串联怎么安排接口自动化中经常遇到场景先创建订单拿到订单号再查询订单。这种接口之间的数据依赖我最推荐的处理方式是分层如果依赖关系简单比如创建接口返回的数据后续用例要用就用 fixture 的返回值传递。创建一个订单的 fixture返回订单号后续用例直接以这个 fixture 作为参数。如果依赖关系复杂比如一个业务流程有十几步每一步的数据都要传递给下一步这种用 pytest fixture 也能做但会很绕。可以考虑在 YAML 用例中用变量引用的方式比如${order_id}在执行前用正则替换。核心逻辑就是找出一段“数据准备方法”在用例执行前动态生成变量。我实际项目中用得最多的是 fixture 方案简单直观代码可读性高。只有需要让非开发角色维护复杂场景时才建议引入 YAML 变量替换。4.3 超时重试和网络抖动怎么兜底接口自动化的不稳定很多时候不是接口有问题而是测试环境网络抖动。如果在 CI 上因为这种问题挂掉一个用例不仅误报还影响效率。所以我一般在 HttpClient 上加一层重试机制但前提是重试的请求必须是幂等的。简单实现可以加一个重试装饰器import time from functools import wraps def retry(max_retries3, delay1): def decorator(func): wraps(func) def wrapper(*args, **kwargs): for i in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if i max_retries - 1: raise time.sleep(delay) return wrapper return decorator然后对 GET 这类天然幂等的请求在 HttpClient 的 get 方法上加重试。对于 POST 下单这类请求绝对不能无脑重试否则很可能重复下单。这是接口自动化重试设计里最重要的原则。4.4 团队协作与 CI 集成一个框架一旦在团队内使用就不仅是个人工具了。我在团队里推行这套框架时强制定了三条规则第一所有用例必须能根据日志和报告快速定位问题所以每个用例的 name、id 必须有业务含义不能叫 test_1、test_2。第二新增用例不能影响已有用例。数据驱动模式下新增 YAML 用例必须保证数据隔离尽量使用随机生成的数据或测试环境专属的账号。第三CI 集成时必须指定运行环境变量执行完成后自动归档报告。在 Jenkins 里的流水线逻辑大致是pip install -r requirements.txt pytest --alluredir reports/allure_results --clean-alluredir allure generate reports/allure_results -o reports/allure_report --clean跑完在 Jenkins 上配置一个“allure report”插件直接展示报告。GitLab CI 也可以用类似的方式把 allure_report 作为 artifact 上传。4.5 我踩过的几个坑第一中文乱码问题。接口返回值里有中文在控制台打出来是 \uXXXX 或者乱码这不是接口真乱码是 requests 在没有指定编码时的默认行为。在 HttpClient 里统一处理一下比如拿到响应后设置 resp.encoding utf-8或者用 resp.json() 时指定 ensure_asciiFalse 再序列化输出。第二shared fixture 的副作用。scopesession 的 fixture 如果内部修改了全局状态后面所有用例都会受影响。比如一个用例修改了用户资料另一个用例需要查询默认资料就会出现数据依赖问题。解决办法是每个用例尽量造自己的数据或者用独立测试账号。第三pytest 参数化中文用例名显示乱码。这个问题经常出现在夹具注入了中文 id 的情况运行日志里显示的是一串转义字符。可以用 pytest_collection_modifyitems 钩子函数统一设置编码。# conftest.py def pytest_collection_modifyitems(items): for item in items: item.name item.name.encode(utf-8).decode(unicode_escape) item._nodeid item.nodeid.encode(utf-8).decode(unicode_escape)第四接口返回大 JSON 时断言直接比较整个 body 会非常脆弱稍微有一个动态字段就失败。应该只校验关键字段把动态字段如时间戳、随机数排除或者用正则匹配。4.6 常见问题速查表现象原因解决方案接口报404但手动调用正常base_url 拼接时多或少斜杠在 HttpClient 里统一处理 url 拼接接口报401token 没过期但 header 没传检查 client_with_token fixture 是否正确注入 header用例偶发失败网络抖动或服务端临时异常对幂等 GET 请求添加重试机制响应中文乱码编码未指定统一设置 resp.encoding utf-8不同环境跑起来结果不同环境配置未隔离用 TEST_ENV 环境变量切换 configs 下的配置文件allure 报告没有请求日志日志没有打印到 allure在 HttpClient 中同时打 logger 和 allure.attach一条用例失败导致后续全挂fixture 异常没有隔离将数据准备逻辑放在用例自身减少共享状态最后分享一个小技巧在接口自动化框架里真正让框架“活”起来的不是代码本身而是用例的组织方式。我的习惯是让 YAML 用例文件按业务模块划分每个模块一个文件pytest 测试文件保持轻薄只负责调用核心层去执行 YAML 里的用例。这样无论是后续接 CI还是让非开发角色参与用例维护都不会有太大障碍。如果你正在搭建自己的框架别追求一次性把所有功能做全先把配置管理、请求封装、数据驱动、断言封装这四个基础模块跑通再逐步叠加登录态、重试、并发、报告等能力。框架是一点点长出来的不是一开始就能设计到完美。我搭这套框架时也经历过从简到繁再到简的过程最后稳定下来的就是你现在看到的结构。