做接口测试这几年,我最顺手的一套组合就是 Python + requests。requests 库上手门槛极低,几行代码就能把一个 HTTP 请求发出去,但它能覆盖的接口测试场景却一点都不少:GET、POST 这些基础请求、会话保持、超时重试、SSL 证书处理、文件上传下载,全都内置支持。配合 pytest 组织用例、openpyxl 做数据驱动,一个轻量级的接口自动化测试框架两天就能跑起来。
这篇文章会把我从环境搭建、请求封装、断言设计到数据驱动的完整实操过程讲清楚,也会把我在真实项目里踩过的坑——比如接口频繁 429 限流、中文乱码、pip 安装报错——一并整理出来。适合刚接触接口自动化的测试工程师,也适合想把手动接口测试改造成自动化回归测试的小团队参考。这里用的是社区里最主流的 Python + requests 方案,也是很多接口自动化测试框架的底层依赖,理解了这一层,后面你再去看那些封装好的平台工具会轻松很多。
1. 接口自动化测试的整体设计与选型思路
1.1 接口自动化到底在解决什么问题
在没有接口自动化之前,我们的回归测试方式基本是打开 Postman 一个个手动点,接口少还好,一旦超过二三十个,每轮回归至少要半天。接口自动化解决的核心问题就三个。
第一个是回归效率。开发每次提测都要把老功能重新验证一遍,手工点容易漏,脚本跑一遍只要几分钟,而且不会漏。第二个是多环境验证成本。同一个接口在开发环境、测试环境、预发布环境都要验证,手工切环境改地址很痛苦,脚本里把环境抽成配置之后,一条命令切换。第三个是问题定位速度。手工测试发现接口报错,还得看请求参数有没有填对、是不是环境问题,接口自动化可以直接把请求和响应都落到日志里,出问题看日志就知道是入参错了还是服务端返回错了。
说句实在话,接口自动化替代不了 UI 自动化和功能测试,它管的是服务端逻辑、参数校验、数据流转这些问题,页面交互和视觉呈现它管不了。但它的投入产出比在自动化测试里是最高的,接口稳定、执行快、报错定位直接,所以绝大多数团队做自动化测试的首选切入点就是它。
1.2 为什么选 Python + requests 而不是其他方案
这个问题几乎每次分享都会被问到,尤其是团队里有人提出用 Java 或者直接用 Postman 批量跑的时候。我做了一个对比,结论其实很清楚。
| 方案 | 学习成本 | 断言灵活性 | 报告与数据驱动 | 典型场景 |
|---|---|---|---|---|
| Python + requests + pytest | 低 | 高,代码随便写 | 强,生态丰富 | 接口自动化回归 |
| Java + HttpClient / RestAssured | 中高 | 高 | 强 | 已有 Java 技术栈的团队 |
| Postman + Newman | 极低 | 弱,脚本能力有限 | 中 | 临时验证、轻量回归 |
| JMeter | 中 | 弱,主要靠插件 | 中 | 压测为主,兼做功能验证 |
requests 最大的优势是语法接近自然语言,读代码几乎不用查文档。比如发送一个 POST 请求,requests 写法就是requests.post(url, json=payload, headers=headers)一行,Java 那边要建连接、设请求头、处理响应,少说十几行。这不是说 Java 不好,而是 Python 在“代码量、学习成本、生态配合”三者的平衡上最舒服。
另外还要说一句,requests 是 Python 里最常用的 HTTP 库,做爬虫采集、数据拉取的也都在用它,同一个库在测试场景和数据采集场景通用,这也是它社区资料特别多的原因。技术选型不建议追求大而全,先把核心链路的用例跑起来比什么都重要。
1.3 框架设计坚持的三个原则
我搭这套框架之前先定了三条原则,后面所有代码都是围绕它们展开的,建议你搭框架前也先想清楚自己的原则。
第一条,数据与代码分离。测试用例的输入数据、期望结果放在 Excel 或 YAML 文件里,测试代码只负责执行和校验。这样做的好处是,测试人员不用写代码也能维护用例,改参数、加用例都不动代码。
第二条,统一请求入口和统一日志。所有接口请求必须走同一个封装方法,不能在每个用例里各写各的 requests 调用。统一入口之后,加日志、加重试、加认证都只改一个地方,这个投入非常值得。
第三条,断言分层。接口的校验必须分开看:HTTP 状态码是一层,业务状态码是一层,关键字段内容又是一层。三层分开校验,出问题时能快速判断是网络问题、服务端逻辑问题,还是数据问题。
这三条原则是我踩了不少坑之后总结出来的。最开始我也在用例里直接写requests.get(),写了几十个用例之后想加个日志,改了十几处,从那以后我就再也不这么干了。
2. 环境准备与 requests 基础实操
2.1 Python 环境、编辑器与 requests 安装
如果你机器上还没有 Python,先去官网下载对应系统的安装包,安装时有一个特别重要的勾选项:Add Python to PATH。不勾的话,命令行里敲python会提示找不到命令。装完打开命令行,先验证一下:
python --version pip --version只要能正常输出版本号,环境就 OK 了。然后安装 requests:
pip install requests如果你在 Windows 上见到Defaulting to user installation because normal site-packages is not writeable这样的提示,不用慌,意思是当前 Python 目录没有写入权限,pip 自动把包装到了当前用户的目录里,requests 一样能正常 import,只是要留意你实际用的解释器是哪一个。国内网络环境下载慢的话,可以指定清华源:pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple,速度会快很多。
编辑器我用的是 VSCode,配置很省事:装一个 Python 扩展,打开项目后用Ctrl+Shift+P执行 “Python: Select Interpreter” 选对解释器。如果你之前装过多个 Python 版本,这一步最容易翻车,选错解释器后面ModuleNotFoundError就会找上你。选完之后创建一个.vscode/settings.json文件,把默认解释器路径固定下来,整个团队的开发体验会一致很多。
最后验证一下 requests 装没装好:
pip list | findstr requests python -c "import requests; print(requests.__version__)"看到版本号输出,就可以开始写第一个请求了。
2.2 用 requests 发送第一个 GET 和 POST 请求
看一个最简单的 GET 请求,这里直接用带参数的写法:
import requests url = "https://api.example.com/user/list" params = {"page": 1, "size": 20} resp = requests.get(url, params=params, timeout=10) print(resp.status_code) print(resp.json())params会被 requests 自动拼接到 URL 后面。timeout=10一定不要省,它限制的是连接和读取的总超时时间,没有这个参数,接口万一卡住,你的测试脚本会一直挂在那里,测试套件越跑越久都查不出原因。
POST 请求类似,重点是json参数:
import requests url = "https://api.example.com/user/login" payload = {"username": "tester", "password": "123456"} resp = requests.post(url, json=payload, timeout=10) print(resp.json())json=payload会自动把 Python 字典序列化成 JSON 格式,并且把Content-Type设成application/json,比手动组装 JSON 字符串靠谱得多。响应对象上有几个常用属性要记清楚:resp.status_code拿状态码,resp.json()拿结构化数据,resp.text拿纯文本,resp.headers拿响应头。注意resp.json()在响应体不是合法 JSON 时会抛异常,调用前最好判断一下resp.status_code和Content-Type。
还有一个细节,很多接口的返回数据格式是{"code": 0, "message": "success", "data": {...}},别直接把整个响应体当成结果,要按层级取,比如resp.json()["data"]["token"]。这个习惯从现在开始就建立起来,后面做断言会省很多事。
2.3 会话保持与登录鉴权处理
HTTP 协议本身是无状态的,接口要识别你是谁,靠的是 Cookie 或者 Token。requests 里处理这个问题最优雅的方式是使用requests.Session():
import requests s = requests.Session() login_url = "https://api.example.com/user/login" s.post(login_url, json={"username": "tester", "password": "123456"}) # 后续请求直接复用同一会话,cookie 会自动带上 resp = s.get("https://api.example.com/order/list", timeout=10) print(resp.json())Session 做了两件非常重要的事:自动保存服务端下发的 Cookie,同一会话内复用底层 TCP 连接。第二点的收益很多人忽略,接口自动化用例多的时候,连接复用能明显降低整体耗时。
如果接口用的是 Token 鉴权,做法是在头部统一带上Authorization字段,一般格式是Token或Bearer <token>:
s.headers.update({"Authorization": "Bearer xxxxxx"})我在项目里通常会把登录封装成一个方法,返回一个已经带好鉴权信息的 Session 对象,所有用例通过 fixture 拿到它,这样用例代码里完全看不到登录和鉴权逻辑。这个方法在第 4 部分会完整展示。
3. 核心模块实现:封装、断言与数据驱动
3.1 统一请求封装:让每个用例只写业务
统一封装的目的是把容易出错、重复度高的逻辑收敛到一个方法里,我把这个类命名为RequestUtil,核心思路是接收方法名、URL、参数、请求体这些入参,内部统一处理超时、日志、异常和返回。代码不长,但非常关键:
import requests import time import logging logger = logging.getLogger("api_test") class RequestUtil: def __init__(self, session: requests.Session = None): self.session = session if session else requests.Session() def send_request(self, method: str, url: str, **kwargs): method = method.upper() kwargs.setdefault("timeout", 10) start = time.time() try: resp = self.session.request(method, url, **kwargs) cost = round((time.time() - start) * 1000, 2) logger.info("请求 %s %s 耗时 %sms", method, url, cost) logger.info("响应状态: %s", resp.status_code) logger.info("响应内容: %s", resp.text[:500]) return resp except requests.exceptions.RequestException as e: logger.error("请求异常: %s", e) raise用的时候,用例只关心业务参数:
util = RequestUtil(session=login_session) resp = util.send_request("GET", "/order/list", params={"page": 1})你一定注意到了,封装里已经用logging把请求和响应都打出来了,这就是统一入口的价值。后续要加重试、加统计,不需要改用例代码,只改这一个类就够了。
3.2 断言设计:三层校验体系
很多新手写接口自动化,断言就一句assert resp.status_code == 200,这远远不够。我见过不少接口,业务逻辑挂了照样返回 HTTP 200,真正的错误放在响应体的code字段里,所以必须把断言分层。
第一层是 HTTP 状态码,代表网络链路通不通;第二层是业务状态码,比如code == 0表示业务成功;第三层是核心字段内容,校验数据对不对。我把这三层断言封装成一个工具方法:
import pytest def assert_result(resp, expect_code=200, expect_biz_code=None, expect_fields=None): # 第一层:HTTP 状态码 assert resp.status_code == expect_code, f"HTTP状态码异常: {resp.status_code}" data = resp.json() # 第二层:业务状态码 if expect_biz_code is not None: assert data.get("code") == expect_biz_code, f"业务码异常: {data.get('code')}" # 第三层:核心字段 if expect_fields: for key, value in expect_fields.items(): actual = data.get("data", {}).get(key) assert actual == value, f"字段 {key} 期望 {value}, 实际 {actual}"三层分开写还有个好处:用例失败时,失败信息直接告诉你是哪一层挂了。实际工作中,接口的期望结果一般不会写死在代码里,而是跟着用例数据走,这就是下面要说的数据驱动。
3.3 数据驱动:用 Excel 管理测试用例
接口自动化的用例如果写在代码里,维护成本会非常高。我把用例数据抽到了 Excel 里,每行一条用例,字段包括:用例编号、接口路径、请求方法、请求参数、期望状态码、期望业务码、期望字段。测试代码只负责读取和组装。
读取 Excel 我推荐openpyxl,读写都很方便:
pip install openpyxlimport json from openpyxl import load_workbook def read_cases_from_excel(file_path, sheet_name): wb = load_workbook(file_path) ws = wb[sheet_name] rows = list(ws.iter_rows(values_only=True)) headers = [str(h).strip() for h in rows[0]] cases = [] for row in rows[1:]: row_map = {headers[i]: row[i] for i in range(len(headers))} # Excel 读出来的参数是字符串,转成 dict row_map["params"] = json.loads(row_map["params"]) cases.append(row_map) return cases注意这里有个坑:Excel 里读出来的params列是字符串,必须用json.loads转成字典再传给 requests,这个就是咱们在热词里常见的“python类型转换”,在数据驱动场景里几乎是必做的操作。
配合 pytest 的参数化,几十条用例只需要一个测试函数:
import pytest from common.requests_util import RequestUtil from common.assert_util import assert_result from common.excel_util import read_cases_from_excel cases = read_cases_from_excel("data/login_cases.xlsx", "login") @pytest.mark.parametrize("case", cases, ids=lambda c: c["case_id"]) def test_login(case, login_session): util = RequestUtil(session=login_session) resp = util.send_request(case["method"], case["url"], json=case["params"]) assert_result( resp, expect_code=case["expect_status"], expect_biz_code=case["expect_biz_code"], )如果你的团队技术基础好一些,也可以直接用 YAML 管理用例,结构更灵活,嵌套深的参数也不用把字符串转来转去。但对多数团队来说,Excel 是门槛最低的方案,业务同学也能参与维护。
3.4 日志与报告输出:让失败可追溯
用例跑完了,怎么让别人看懂执行结果?我通常做两件事:日志落盘和 HTML 报告。
日志用 Python 内置logging,配置成控制台输出 INFO 级别、文件里输出 DEBUG 级别。除了第 3.1 节里封装的请求日志,我还会在用例的关键步骤里打日志,比如登录成功拿到了 token、订单状态是已支付等。这样测试失败时,打开日志文件就能看到完整上下文,而不是只看到一个断言失败的堆栈。
报告就用 pytest-html,一条命令的事:
pip install pytest-html pytest testcases -s -v --html=reports/result.html打开生成的 HTML 报告,每个用例的执行结果、失败原因、耗时都清清楚楚。报告和日志要能对应起来:报告里写清用例编号,日志里同样带上用例编号,排查问题时两边一对,效率特别高。
如果你们团队对报告颜值和趋势统计有要求,可以再上 Allure,功能确实更强,但配置成本也高。我的建议是先把 pytest-html 用起来,东西跑顺了再升级。
4. 实战:搭一套登录到下单的接口自动化框架
4.1 项目目录结构
这里我给你一套可以直接抄作业的目录结构,按这个组织,项目清晰也不容易乱。
api_test/ ├── common/ │ ├── __init__.py │ ├── requests_util.py │ ├── assert_util.py │ ├── excel_util.py │ └── log_util.py ├── data/ │ ├── login_cases.xlsx │ └── order_cases.xlsx ├── testcases/ │ ├── __init__.py │ ├── conftest.py │ ├── test_login.py │ └── test_order.py ├── logs/ ├── reports/ ├── requirements.txt └── pytest.ini我来解释一下每个目录的职责。common放公共封装,原则上不允许出现业务逻辑;data只放测试数据,用 Excel 表达;testcases放 pytest 用例和 fixture;logs和reports是运行产物,一般加进.gitignore不进代码库。pytest.ini配置 pytest 的参数,比如日志级别、用例匹配规则。
4.2 核心代码实现说明
请求封装和断言工具在前面已经给了完整代码,这里重点说一下conftest.py里的登录 fixture,这是整个框架复用性最高的地方:
import pytest import requests from common.requests_util import RequestUtil @pytest.fixture(scope="session") def login_session(): s = requests.Session() url = "https://api.example.com/user/login" resp = s.post(url, json={"username": "tester", "password": "123456"}, timeout=10) assert resp.status_code == 200 s.headers.update({"Authorization": f"Bearer {resp.json()['data']['token']}"}) yield s s.close()scope="session"意味着整个测试会话只登录一次,后续测试全部复用这个带鉴权的 Session,既能控制登录接口的调用频率(避免触发限流),又能大幅减少用例执行时间。
test_order.py里的下单用例长这样:
import pytest from common.requests_util import RequestUtil from common.assert_util import assert_result from common.excel_util import read_cases_from_excel cases = read_cases_from_excel("data/order_cases.xlsx", "order") @pytest.mark.parametrize("case", cases, ids=lambda c: c["case_id"]) def test_create_order(case, login_session): util = RequestUtil(session=login_session) resp = util.send_request(case["method"], case["url"], json=case["params"]) assert_result( resp, expect_code=case["expect_status"], expect_biz_code=case["expect_biz_code"], expect_fields=case.get("expect_fields"), )用例代码不到二十行,但跑起来能支撑上百条用例,这就是封装和数据驱动的威力。业务要加新用例,不需要动代码,往 Excel 里加一行就行。
4.3 运行、报告与持续集成
在项目根目录执行:
pytest testcases -s -v --html=reports/result.html --self-contained-html--self-contained-html这个参数建议加上,它会把 CSS 和 JS 全部内嵌到 HTML 文件里,报告单文件就能发给别人看,不会出现样式丢失的问题。
跑完看一眼报告的通过率、失败用例的断言信息,再把日志目录里的日志打开和用例编号对一下,基本就能定位问题。日常迭代阶段,我习惯每天下班前全量跑一遍接口回归,早上来了先看报告有没有红。
这套框架接持续集成也很简单,我在团队内部是把它接到代码托管平台的流水线任务里,每次提交代码自动触发,跑完自动归档报告,失败时通过企业微信机器人推一条消息到测试群。这一部分等你有 CI 环境了再折腾,先把本地跑通更实在。
5. 常见问题与排查技巧实录
5.1 接口频繁报 429 Too Many Requests 怎么办
这个报错我猜很多人都遇到过,脚本跑着跑着突然来一句exceeded retry limit, last status: 429 too many requests,看起来是重试次数用尽之后的完整报错。429 的意思是你单位时间内请求太频繁,服务端主动限流了。登录接口、验证码接口、第三方开放接口最容易触发。
处理方案有两个方向。第一个是代码层面加重试和退避,requests 可以通过HTTPAdapter挂上 urllib3 的Retry:
import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry retry = Retry( total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504], ) s = requests.Session() s.mount("http://", HTTPAdapter(max_retries=retry)) s.mount("https://", HTTPAdapter(max_retries=retry))这里backoff_factor=1的含义要理解清楚,每次重试的等待时间是backoff_factor * (2 ** (重试次数 - 1)),也就是说第一次重试等 1 秒,第二次 2 秒,第三次 4 秒,呈指数退避。这个设计是为了给服务端留出恢复时间,如果退避间隔不够,重试得越频繁反而越容易被限流,甚至被封。
第二个方向是业务层控制请求频率。批量测试时,在用例里加一个可控的time.sleep(0.2),或者用线程池加并发上限,都能显著降低 429 的发生率。我的经验是:触发 429 后,先把重试次数调小,优先从业务频率上找原因,别一上来就堆重试参数。
5.2 中文乱码与编码问题
接口返回内容里带中文,打印出来全是“这种乱码,大概率是编码猜错了。requests 在响应头没有明确给出charset的时候,会默认按 ISO-8859-1 去解码,中文自然会乱码。解决办法是在读取响应前手动指定编码:
resp.encoding = "utf-8" # 或者让 requests 根据内容自动推断 # resp.encoding = resp.apparent_encoding print(resp.text)apparent_encoding是根据响应内容的字节特征推断编码,准确率比较高,但有性能开销,大规模跑测试时建议直接写死utf-8,绝大多数现代接口都是这个编码。
还有个容易踩的坑是用 CSV 写测试数据或测试结果,用openpyxl写 Excel 没事,但如果你用 csv 模块,写入时一定要用encoding="utf-8-sig",否则生成的 CSV 用 Excel 打开中文全是乱码:
import csv with open("result.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["用例编号", "结果", "耗时"])5.3 SSL 证书校验报错
自建测试环境经常用自签名证书,requests 请求时会报SSLError: certificate verify failed。处理方式是在请求参数里加上verify=False:
resp = requests.get(url, verify=False)但这样每次都会刷一条InsecureRequestWarning警告,可以把全局警告关掉:
import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)这里必须说清楚:verify=False会跳过证书校验,有中间人攻击的风险,生产环境和外网接口绝对不能这么干。内网测试环境可以用,但代码里要写注释说明原因。更稳妥的做法是把测试环境的 CA 证书文件放到项目里,用verify="path/to/ca.pem"指定证书路径,既绕过报错又保留了校验能力。
5.4 pip 安装、依赖管理与解释器切换
第 2 节提到的Defaulting to user installation提示,本质是当前 Python 环境没有磁盘写权限。虽然不影响使用,但多人协作或多个 Python 版本共存时,很容易出现“我明明装了 requests,运行时却 ModuleNotFoundError”的情况,根源就是装到了 A 解释器,跑的是 B 解释器。
解决这个问题最干净的方式是用虚拟环境:
python -m venv venv # Windows 激活 venv\Scripts\activate # macOS/Linux 激活 source venv/bin/activate pip install -r requirements.txt虚拟环境的好处是每个项目一套独立的依赖,互不污染。依赖清单用pip freeze > requirements.txt生成,新同事拉下代码后执行pip install -r requirements.txt就能把所有依赖装齐。
在 VSCode 里遇到ModuleNotFoundError,先看右下角解释器是不是你 venv 里的那个,快捷键Ctrl+Shift+P执行 “Python: Select Interpreter” 重新选一次,90% 的问题都能解决。还有个小技巧,把pytest.ini里加上python_files = test_*.py,避免 pytest 误收集非测试文件。
我在实际维护这套框架的过程中最大的体会是,接口自动化的难点从来不在 requests 怎么发请求,而在于用例的组织和断言的设计。框架的封装越通用,写用例的人负担越轻,这套东西才越有可能持续跑下去。最后再分享一个小习惯:每轮测试跑完一定要留日志,requests 的响应体会比较多,我落盘之前会截断一下,只保留前 500 个字符,既保证能定位问题,又不至于把日志文件撑得太大。如果你也刚开始搭接口自动化,建议先别追求复杂的平台化,就用 requests 加 pytest 把一条核心业务链路的用例跑起来,维护一两个月之后,你自然知道下一步该加什么功能。