做接口测试这几年我最顺手的一套组合就是 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, jsonpayload, headersheaders)一行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 了。然后安装 requestspip 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 扩展打开项目后用CtrlShiftP执行 “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, paramsparams, timeout10) print(resp.status_code) print(resp.json())params会被 requests 自动拼接到 URL 后面。timeout10一定不要省它限制的是连接和读取的总超时时间没有这个参数接口万一卡住你的测试脚本会一直挂在那里测试套件越跑越久都查不出原因。POST 请求类似重点是json参数import requests url https://api.example.com/user/login payload {username: tester, password: 123456} resp requests.post(url, jsonpayload, timeout10) print(resp.json())jsonpayload会自动把 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, timeout10) print(resp.json())Session 做了两件非常重要的事自动保存服务端下发的 Cookie同一会话内复用底层 TCP 连接。第二点的收益很多人忽略接口自动化用例多的时候连接复用能明显降低整体耗时。如果接口用的是 Token 鉴权做法是在头部统一带上Authorization字段一般格式是Token或Bearer tokens.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(sessionlogin_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_code200, expect_biz_codeNone, expect_fieldsNone): # 第一层HTTP 状态码 assert resp.status_code expect_code, fHTTP状态码异常: {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_onlyTrue)) 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, idslambda c: c[case_id]) def test_login(case, login_session): util RequestUtil(sessionlogin_session) resp util.send_request(case[method], case[url], jsoncase[params]) assert_result( resp, expect_codecase[expect_status], expect_biz_codecase[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 --htmlreports/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 用例和 fixturelogs和reports是运行产物一般加进.gitignore不进代码库。pytest.ini配置 pytest 的参数比如日志级别、用例匹配规则。4.2 核心代码实现说明请求封装和断言工具在前面已经给了完整代码这里重点说一下conftest.py里的登录 fixture这是整个框架复用性最高的地方import pytest import requests from common.requests_util import RequestUtil pytest.fixture(scopesession) def login_session(): s requests.Session() url https://api.example.com/user/login resp s.post(url, json{username: tester, password: 123456}, timeout10) assert resp.status_code 200 s.headers.update({Authorization: fBearer {resp.json()[data][token]}}) yield s s.close()scopesession意味着整个测试会话只登录一次后续测试全部复用这个带鉴权的 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, idslambda c: c[case_id]) def test_create_order(case, login_session): util RequestUtil(sessionlogin_session) resp util.send_request(case[method], case[url], jsoncase[params]) assert_result( resp, expect_codecase[expect_status], expect_biz_codecase[expect_biz_code], expect_fieldscase.get(expect_fields), )用例代码不到二十行但跑起来能支撑上百条用例这就是封装和数据驱动的威力。业务要加新用例不需要动代码往 Excel 里加一行就行。4.3 运行、报告与持续集成在项目根目录执行pytest testcases -s -v --htmlreports/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 的Retryimport requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry retry Retry( total3, backoff_factor1, status_forcelist[429, 500, 502, 503, 504], ) s requests.Session() s.mount(http://, HTTPAdapter(max_retriesretry)) s.mount(https://, HTTPAdapter(max_retriesretry))这里backoff_factor1的含义要理解清楚每次重试的等待时间是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 模块写入时一定要用encodingutf-8-sig否则生成的 CSV 用 Excel 打开中文全是乱码import csv with open(result.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([用例编号, 结果, 耗时])5.3 SSL 证书校验报错自建测试环境经常用自签名证书requests 请求时会报SSLError: certificate verify failed。处理方式是在请求参数里加上verifyFalseresp requests.get(url, verifyFalse)但这样每次都会刷一条InsecureRequestWarning警告可以把全局警告关掉import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)这里必须说清楚verifyFalse会跳过证书校验有中间人攻击的风险生产环境和外网接口绝对不能这么干。内网测试环境可以用但代码里要写注释说明原因。更稳妥的做法是把测试环境的 CA 证书文件放到项目里用verifypath/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 里的那个快捷键CtrlShiftP执行 “Python: Select Interpreter” 重新选一次90% 的问题都能解决。还有个小技巧把pytest.ini里加上python_files test_*.py避免 pytest 误收集非测试文件。我在实际维护这套框架的过程中最大的体会是接口自动化的难点从来不在 requests 怎么发请求而在于用例的组织和断言的设计。框架的封装越通用写用例的人负担越轻这套东西才越有可能持续跑下去。最后再分享一个小习惯每轮测试跑完一定要留日志requests 的响应体会比较多我落盘之前会截断一下只保留前 500 个字符既保证能定位问题又不至于把日志文件撑得太大。如果你也刚开始搭接口自动化建议先别追求复杂的平台化就用 requests 加 pytest 把一条核心业务链路的用例跑起来维护一两个月之后你自然知道下一步该加什么功能。