做了这么多年接口自动化,我见过太多项目死在框架选型这一步。有的团队听说 httpx 出来了就赶紧把 requests 换掉,结果一堆老接口的兼容问题全冒出来了;有的被 unittest 的 setUp、tearDown 组织方式折磨到怀疑人生,却不知道 pytest 的 fixture 能省掉多少重复代码;还有的为了“显得专业”硬上 YAML 做数据驱动,最后用例多到没人愿意维护。这个 Python + Requests + Pytest + Excel + Allure 的组合,是我这几年给多个团队搭接口自动化框架时沉淀下来的相对稳妥的一套,它的核心优势并不是每一个组件都最时髦,而是每一层的能力都卡在了一个平衡点上:Requests 负责稳定地发 HTTP 请求,Pytest 负责测试用例的组织与执行,Excel 负责测试数据的承载和维护,Allure 负责把执行结果变成一份团队看得懂的报告。这篇文章我会把“框架之间的对比”作为主线,从选型思路、完整项目骨架,到真实环境下的踩坑经验,一步一步拆给你看。
这套东西适合谁?如果你正准备从零搭建接口自动化,或者已经在用 unittest 写用例但觉得维护成本越来越高,又或者你把接口用例写在代码里却发现数据和代码纠缠得没法看,那么这篇文章基本就是照着你的痛点写的。整个过程中我不会只给结论,会尽量把每一个对比维度背后的取舍逻辑讲清楚,方便你在自己的团队里做决策。
1. 先想清楚:接口自动化框架到底在比什么
很多人在选型的时候容易陷入“工具名气对比”的误区。今天看一篇文章说 httpx 支持 HTTP/2 很香,明天又看一个视频说 unittest 是标准库不需要额外装依赖,后天看到 robotframework 的关键字风格被领导夸了,于是整个技术方案被各种零散信息牵着走。要避免这种状态,第一步不是打开搜索引擎,而是先把“我们的接口自动化项目需要框架做什么”完整盘一遍。
1.1 一套接口自动化框架的五个能力模块
我习惯把接口自动化框架拆成五个能力模块:
- HTTP 客户端能力:能发 GET、POST、PUT、DELETE 请求,能处理 header、cookie、query 参数、请求体,能设置超时和重试。
- 测试执行能力:能组织用例、支持前后置动作、能写断言、能统计通过率和失败原因。
- 测试数据管理能力:测试数据不能硬编码在脚本里,要能外置、能批量维护、能切换环境。
- 报告与可视化能力:执行结果要能变成团队成员能看懂的报告,而不是一堆没人打开的 log。
- CI 集成能力:能够被命令行一键驱动,能够接入 Jenkins、GitLab CI 等流水线,让自动化在持续集成里稳定跑。
这五个模块,每一个都有多个框架可以选,它们之间本来没有“谁绝对好”的关系,只有“谁更适合你当前的场景和团队结构”的关系。所以整个标题里的“Python + Requests + Pytest + Excel + Allure”本质上不是一个框架,而是五个位置上的五个选型。真正的对比发生在每一个位置上:HTTP 客户端层选什么、测试执行层选什么、数据管理层选什么、报告层选什么。
1.2 为什么这套组合在多数项目里够用
我做了不少项目之后发现一个规律:接口自动化的技术复杂度远低于业务复杂度。大多数企业内部接口自动化项目,请求量一天也就几千次,并发并不高,真正难的是业务状态的串联、数据环境的稳定、断言口径的统一。也就是说,性能极限通常不是瓶颈,可维护性和可读性才是。
在这个前提下:
- Requests 提供的同步请求模型足够应付绝大多数接口自动化场景,它的 API 设计是“人类友好”的。
- Pytest 的 fixture 和参数化机制,可以让前后置逻辑和数据驱动变得非常清爽。
- Excel 虽然不是最有“技术感”的数据载体,但它在团队协作里优势很大——产品和测试都能直接改,不需要打开代码编辑器。
- Allure 则解决了测试报告“只给自己看”的问题,步骤、层级、历史趋势都不需要额外开发。
这套组合没有一个组件是“同类型里最强的”,但组合起来非常顺手,这也是我在这个标题里把“框架之间的对比”放在括号里的原因——真正值得对比的是每个位置上的选型,而不是拿着一个框架去吊打另一个。
2. Requests 凭什么站在 HTTP 客户端的第一梯队
HTTP 客户端层是接口自动化的地基。这一层选得不好,后面所有用例写起来都会很别扭。在 Python 生态里,可选对象大概有 urllib、Requests、httpx、aiohttp 这么几个,下面把这几个放在一起做一轮对比。
2.1 urllib、Requests、httpx、aiohttp 的真实差异
先看代码层面的直观感受。用 urllib 发一个带 JSON 体的 POST 请求,你需要这样写:
import json import urllib.request data = json.dumps({"username": "admin", "password": "123456"}).encode("utf-8") req = urllib.request.Request( "https://api.example.com/login", data=data, headers={"Content-Type": "application/json"} ) with urllib.request.urlopen(req, timeout=5) as resp: body = resp.read().decode("utf-8") status = resp.status用 Requests 写同样的逻辑:
import requests resp = requests.post( "https://api.example.com/login", json={"username": "admin", "password": "123456"}, timeout=5 ) status = resp.status_code body = resp.json()同样是发一个请求,代码量差距接近一半,可读性差距更大。urllib 不是不能用,而是在项目里会浪费大量开发时间去处理编码、响应解析、cookie 管理这些底层细节,这些工作对接口用例本身没有任何正向帮助。
再看 httpx 和 aiohttp。httpx 支持 HTTP/2,也有异步模式,但如果你的接口自动化是同步执行,它的核心优势根本用不上,反而多了一个学习成本。aiohttp 是异步框架,性能上限高,但异步代码的调试成本、团队成员熟悉程度都要考虑,为了一个并发量并不高的接口自动化项目引入异步复杂度并不划算。
下面这张表是我在实际项目中对比后留下的参考:
| 维度 | urllib | Requests | httpx | aiohttp |
|---|---|---|---|---|
| 上手成本 | 中等,标准库自带 | 极低 | 较低 | 较高,需要异步基础 |
| 会话管理 | 手动维护 Cookie | Session 自动处理 | Client 自动处理 | 需要自己维护 session |
| 同步请求体验 | 僵硬 | 自然 | 自然 | 以异步为主 |
| HTTP/2 | 不支持 | 不支持 | 支持 | 不支持(另有 h2 支持) |
| 超时/重试 | 需要手写 | 通过 HTTPAdapter 配置 | 类似 Requests | 需要手写 |
| 团队替换成本 | 低 | 低 | 中 | 高 |
2.2 Session 的工程化价值
Requests 库真正拉开差距的不是单个请求怎么发,而是requests.Session()这套机制。Session 会自动保存 cookie,复用底层 TCP 连接,还可以统一设置 headers、基础域名、超时时间。
实际项目里我基本不会裸用requests.get(),而是先构建一个项目级的 Session 对象:
import requests import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) session = requests.Session() session.headers.update({ "Content-Type": "application/json", "User-Agent": "AutoTest/1.0" }) session.verify = False这个 Session 对象在后续所有用例里共享,测试登录后拿到的 token 只要塞进session.headers,后面的请求就都会自动带上,不用每个用例手动传一遍。接口自动化里最常见的一类重复劳动,就被这个机制简化掉了。
需要特别提醒一句:verify=False是因为很多企业内部测试环境是自签名证书,不关掉验证用例根本跑不起来。关掉之后配合urllib3.disable_warnings去掉控制台警告,这是常规做法,但到了生产环境或对外服务,证书校验必须打开。
3. Pytest 和 unittest 的差距不在断言,而在用例组织方式
测试执行层是整个框架里争议最大的位置。unittest 是 Python 标准库,零依赖,很多老项目都在用;Pytest 是第三方库,装一个依赖但换来了一整套工程化能力。我的观点很明确:新项目直接 Pytest,没有任何犹豫的必要。
3.1 fixture 是 Pytest 与 unittest 的分水岭
unittest 的前置和后置靠setUp、tearDown、setUpClass、tearDownClass这些固定方法名。它的最大问题是:作用域是写死的,不同用例类的公共前置逻辑要么复制粘贴,要么写一个公共基类去继承,时间长了类继承关系乱得一塌糊涂。
Pytest 的 fixture 把前后置动作变成了一种“可声明的依赖”。我举一个很常见的例子——大部分接口用例都需要一个登录后的 token:
import pytest import requests @pytest.fixture(scope="session") def login_token(): resp = requests.post("https://api.example.com/login", json={"username": "admin", "password": "123456"}) assert resp.status_code == 200 return resp.json()["token"] def test_get_user_info(login_token): resp = requests.get( "https://api.example.com/user/info", headers={"Authorization": f"Bearer {login_token}"} ) assert resp.status_code == 200 assert resp.json()["code"] == 0注意scope="session"这个参数,它会保证整个测试会话只登录一次,token 被缓存并复用。如果这个逻辑放在 unittest 里,要么用setUpClass配合类变量去实现,要么干脆在模块顶层登录一次,这两种做法都非常容易写坏。fixture 的依赖关系是显式的,用例函数把login_token作为参数填上去,它就知道自己依赖什么,不需要任何类继承关系。
3.2 参数化让数据驱动成为可能
接口自动化的核心模式是数据驱动——测试步骤和测试数据分离。Pytest 的参数化机制是这个模式的地基:
import pytest @pytest.mark.parametrize( "username,password,expected_code", [ ("admin", "123456", 0), ("admin", "wrong", 1001), ("", "123456", 1002), ] ) def test_login_cases(username, password, expected_code): resp = requests.post("/login", json={"username": username, "password": password}) assert resp.json()["code"] == expected_code一条用例函数可以跑多个数据组合,测试报告里每个组合会单独显示。对比 unittest 时代常用的ddt库,Pytest 的参数化是原生能力,不需要额外封装装饰器,也不需要担心 IDE 跳转失效。
3.3 conftest 和插件生态:工程化的分水岭
当用例量大到一定规模,一定会遇到两个问题:公共的 fixture 怎么共享,用例执行顺序怎么控制。
Pytest 的conftest.py可以实现在不经import的情况下跨文件共享fixture。我在项目里的做法是:
- 项目根目录放一个
conftest.py,定义全局级别的 fixture(比如日志对象、数据库连接)。 testcase目录下放一个conftest.py,定义模块级别的 fixture(比如登录 token、前置数据准备)。
写完 fixture 直接在测试函数签名里声明参数就能用,Pytest 会自动解析。这种机制带来的好处是,新成员看代码时不需要通过复杂的类继承关系去追踪“哪个数据是从哪来的”,直接看函数参数就能掌握依赖。
插件生态是另一个加分项。pytest-xdist用来并行执行用例、pytest-ordering控制用例顺序、pytest-assume解决断言不中断的问题、pytest-rerunfailures失败重跑——每一样都是接口自动化项目里的刚需场景,而这些能力在 unittest 里都需要自己造轮子。
3.4 对比一下 robotframework
很多人会问 robotframework 要不要考虑。它的关键字驱动风格对非技术人员确实友好,团队成员不需要懂 Python 语法就能写用例。但我个人不太建议在纯接口自动化项目里用它,原因是:接口自动化的用例需要大量逻辑判断、动态参数、数据拼接,这些东西用关键字拼装表达起来很别扭,调试也会比直接写 Python 麻烦一个量级。robotframework 更适合 UI 自动化或者业务流程类的测试场景,接口自动化项目用 Pytest 就够了。
4. Excel 不是土办法,而是团队协作的最优解
数据驱动的实质是让测试数据和测试代码分离。数据可以放在 Excel、JSON、YAML、数据库里,每一种都有人用。这里先给一个结论:没有绝对正确的选择,只有更适合当前团队和数据形态的选择。
4.1 各数据源方案的适用边界
| 数据源 | 可维护性 | 可读性 | 动态生成能力 | 适用场景 |
|---|---|---|---|---|
| Excel | 高,业务人员可维护 | 高 | 弱,数据本身不变 | 中小规模用例,多字段组合的接口测试 |
| JSON | 中 | 中 | 中 | 结构化数据,嵌套较深时推荐 |
| YAML | 高 | 高 | 中 | 配置类数据,层级清晰时推荐 |
| 数据库 | 低,需要额外开发维护 | 低 | 强,可动态生成 | 数据量大、依赖前置数据生成的场景 |
Excel 的优势体现在协作上。大多数接口测试用例是测试人员写的,不是开发人员写的,如果要求测试人员维护 JSON 或者 YAML,往往需要额外培训。Excel 是大家日常就在用的工具,测试人员直接在表格里加一行数据、填上请求参数和期望结果,用例就增加了,这种维护门槛几乎为零。
4.2 用 openpyxl 把 Excel 变成用例源
在 Python 里读取 Excel 文件,我通常使用 openpyxl。拿一个登录接口的用例表举例,Excel 的列设计可以包含:用例 ID、请求方法、请求 URL、请求头(可空)、请求体、期望状态码、期望业务码、备注。这样一个 Excel 文件就能完整描述所有登录场景的用例数据。
读取和处理的封装如下:
import json from openpyxl import load_workbook def load_test_data(file_path, sheet_name="Sheet1"): wb = load_workbook(file_path, data_only=True) ws = wb[sheet_name] rows = list(ws.iter_rows(values_only=True)) if not rows: return [] headers = rows[0] cases = [] for row in rows[1:]: if row[0] is None: continue case = dict(zip(headers, row)) # 把字符串形式的 JSON 请求体转成字典 if isinstance(case.get("request_body"), str) and case["request_body"].strip(): case["request_body"] = json.loads(case["request_body"]) cases.append(case) return cases这里有两个细节值得注意:
data_only=True会让 openpyxl 读取单元格展示出来的值,而不是公式本身。如果 Excel 里有函数计算出来的值,这个参数能避免读到公式字符串。- 空行的跳过是必须的,因为 Excel 表格经常会有人误按空格留下“看似空白但实际有空格”的行。
4.3 把 Excel 数据安全送到 Pytest 参数化
读出来的数据要和 Pytest 参数化对接,常规做法是手动拼参数列表:
import pytest from common.excel_util import load_test_data cases = load_test_data("data/test_login.xlsx", "登录用例") @pytest.mark.parametrize("case", cases, ids=[c["case_id"] for c in cases]) def test_login_from_excel(case, login_token_prepare): resp = requests.post( case["request_url"], json=case["request_body"], headers=case.get("request_headers") or {} ) assert resp.status_code == case["expected_status"] assert resp.json()["code"] == case["expected_code"]ids参数让每一条用例在报告中显示 Excel 里的用例 ID,比如LOGIN_001、LOGIN_002。这样测试报告里哪个用例失败了,直接对照 Excel 的那一行就能定位,排查成本大大降低。
4.4 什么情况下放弃 Excel
Excel 不是银弹。当一个接口的请求体嵌套特别深(比如三层以上的字典嵌套),用 Excel 表示会让单元格内容变得非常长且难以读;当测试数据需要动态计算(比如时间戳、随机数、上一步接口响应值),Excel 静态数据就满足不了;当测试用例超过几千条,Excel 的查找和更新效率也会下降。
在这些场景下我建议局部调整方案:动态数据在代码里用 fixture 构造,静态数据继续走 Excel;嵌套深的接口用 YAML 作为补充数据源。框架不一定要二选一,可以按接口模块灵活切换。
5. Allure 报告:从“自测工具”到“团队资产”
报告层是整个框架里最容易被低估的一层。很多人觉得报告只要能看通过率和失败 log 就够了,但实际上,一份好的测试报告会影响整个团队对自动化项目的信任度。Allure 和传统报告工具之间的差距,远比表面看起来大。
5.1 pytest-html、HTMLTestRunner 与 Allure 的本质差异
HTMLTestRunner 是 unittest 时代的老方案,早就停止维护了,报告样式也停留在十年前。pytest-html 可以作为 Pytest 的替代方案使用,但它生成的就是一个扁平结构的 HTML 文件:用例清单、通过失败状态、错误信息。在用例量超过几百条以后,这种报告的问题就暴露了——你可能需要从上到下翻半天才能定位到“失败集中在哪个模块”,也没有任何历史趋势的展示。
Allure 的本质不是一个“HTML 生成器”,而是一套测试报告框架。它把执行过程中产生的数据先收集成中间文件,再统一生成一个可交互的 Web 报告。你可以在报告里按照 epic、feature、story 三层维度去浏览用例结构,可以查看每一步请求的请求体和响应体,可以看历史失败趋势,还可以把失败用例按 severity 分类筛选。对接口测试来说,请求和响应记录尤其关键——排查问题的时候不用再回控制台翻 log。
5.2 Allure 的安装与集成流程
Allure 的安装分为两部分:Python 库和命令行工具。
先安装 Python 库:
pip install allure-pytest再装命令行工具。在 macOS 上可以用brew install allure,Windows 上建议下载 allure commandline 的压缩包,解压后把 bin 目录加入 PATH。装好后用allure --version验证。
集成到 Pytest 只需要在配置文件里指定结果目录,或者运行时加参数:
pytest testcase/ -s -v --alluredir=./allure-results --clean-alluredir--clean-alluredir会在每次执行前清空旧的中间结果,避免新旧报告数据混在一起。执行完成后生成并打开报告:
allure generate ./allure-results -o ./allure-report --clean allure open ./allure-reportgenerate命令把中间结果渲染成 Web 报告,open会启动一个本地服务并在浏览器里打开。如果是在 CI 里执行,把allure generate这一步放到流水线里,生成后的allure-report目录就是可归档的产物。
5.3 epic、feature、story、title:报告层级的设计逻辑
这正是“allure报告标题等级”关注的核心。Allure 报告支持四个层级的关键字,从大到小依次为:
@allure.epic:对应报告里的 Epic 标签,通常代表一个大的产品线或系统。@allure.feature:对应 Feature 标签,通常代表一个功能模块。@allure.story:对应 Story 标签,通常代表一个业务场景。@allure.title:对应用例的显示标题,它决定用例在报告里的展示文本。
实际代码片段:
import allure @allure.epic("电商平台接口自动化") @allure.feature("用户模块") @allure.story("登录") @allure.title("正确账号密码登录") def test_login_success(): ...执行完生成报告后,可以看到报告首页按 epic 分类展示,点进 epic 能看到 feature,点进 feature 能看到 story,最终一层层下钻到实际用例。这种层级设计的价值是把“用例执行结果”和“业务模块结构”映射在一起,领导想看整体质量看 epic 层,测试想排查细节直接进到用例层,效率高很多。
除了静态标题,Allure 还支持动态标题,适合在标题里带上用例名称参数:
@allure.title("登录用例:{case_id}") def test_login(case_id): ...还有@allure.step("操作步骤说明")装饰器,可以把复杂用例拆解成多个步骤步骤。在报告里每个步骤都能独立展开,失败时可以直接定位到是“发请求”失败还是“断言校验”失败。
5.4 一个小插曲:Allure 中的请求响应记录
接口自动化报告比 UI 自动化报告多一个核心诉求:记录请求和响应。Allure 并不自动记录 requests 库的流量,需要借助 fixture 或者钩子把关键信息 attach 到报告里。最简单的做法是把请求参数组装成 JSON 文本,用allure.attach()挂到当前用例上:
import allure import json def attach_request_info(method, url, headers, body, resp): request_info = { "method": method, "url": url, "headers": headers, "body": body, "response_status": resp.status_code, "response_body": resp.text[:2000] } allure.attach( json.dumps(request_info, ensure_ascii=False, indent=2), name="请求与响应", attachment_type=allure.attachment_type.JSON )有了这个附属文件,排查接口问题时就不需要去控制台里翻输出,直接在 Allure 报告里看请求参数是什么、服务端返回了什么,工作效率提升一个档次。
6. 完整项目骨架:从零到能跑
前面把每个组件的前因后果讲了一遍,现在把它们组合起来,构建一个可以直接上手的项目骨架。这个骨架是我在实际项目里反复调整后留下的形态,不追求“花活”,只追求结构清楚、新成员上手快、后续扩展方便。
6.1 目录结构与职责边界
我推荐的目录结构如下:
interface_test/ ├── config/ │ ├── __init__.py │ └── settings.py # 环境切换、域名配置、全局参数 ├── common/ │ ├── __init__.py │ ├── requests_util.py # 请求封装、Session 管理、重试逻辑 │ ├── excel_util.py # Excel 数据读取 │ ├── allure_util.py # 请求响应附属信息 │ └── assert_util.py # 通用断言封装 ├── data/ │ ├── test_login.xlsx │ └── test_user.xlsx ├── testcase/ │ ├── __init__.py │ ├── conftest.py # 局部 fixture │ └── test_login.py │ └── test_user.py ├── reports/ # 生成的 HTML 报告 ├── allure-results/ # 执行产生的中间结果 ├── requirements.txt └── pytest.iniconfig目录放环境相关的配置,例如不同环境的 base_url、账号信息。之所以单独建一个目录,是因为接口自动化项目几乎一定会有多环境切换的需求——测试环境、预发布环境、生产环境(只读接口)都需要跑,环境信息集中管理后切换非常方便:
# config/settings.py import os ENV = os.getenv("TEST_ENV", "test") ENV_CONFIG = { "test": { "base_url": "https://test-api.example.com", "username": "test_user", "password": "test_pass" }, "pre": { "base_url": "https://pre-api.example.com", "username": "pre_user", "password": "pre_pass" } } config = ENV_CONFIG[ENV]6.2 核心代码:请求层、数据层、用例层
common/requests_util.py是整个框架的“发动机”,封装了 Session、统一超时、统一重试,同时预留了日志输出的位置:
# common/requests_util.py import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from config.settings import config def create_session(retry_times=3, backoff_factor=1): session = requests.Session() session.headers.update({"Content-Type": "application/json"}) session.verify = False retry = Retry( total=retry_times, backoff_factor=backoff_factor, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET", "POST", "PUT", "DELETE"] ) adapter = HTTPAdapter(max_retries=retry) session.mount("https://", adapter) session.mount("http://", adapter) return session session = create_session() def request(method, url, **kwargs): kwargs.setdefault("timeout", (5, 30)) full_url = url if url.startswith("http") else config["base_url"] + url resp = session.request(method, full_url, **kwargs) return resp这里需要解释几个参数的含义:
timeout我习惯传一个元组,(5, 30)表示连接超时 5 秒、读超时 30 秒。如果只传一个数字,requests 会把它同时当作连接和读超时,实际使用时容易遇到“接口慢一点就超时”的误报。Retry的backoff_factor是退避因子,重试等待时间按{backoff_factor} * (2 ** (retry_number - 1))计算,1 秒、2 秒、4 秒这样递增。这个策略对服务器限流场景非常有用。allowed_methods需要显式写明,旧版本 Retry 默认只重试 GET。如果 POST 接口在超时后没有做幂等处理,重试可能引发重复提交问题,这里有取舍,但接口测试场景下我通常打开重试并配合 backoff,同时提醒业务方接口要尽量做幂等。
common/excel_util.py和common/allure_util.py的代码在前文已经给过,工程上可以直接照搬。common/assert_util.py可以封装一些高频断言,比如“响应体里某字段的类型”“列表长度”“时间格式”等,减少测试函数里重复写断言模板的负担:
# common/assert_util.py def assert_code(resp, expected_code): assert resp.json().get("code") == expected_code, \ f"业务code断言失败, 期望: {expected_code}, 实际: {resp.json().get('code')}" def assert_success(resp, success_code=0): assert_code(resp, success_code)用例层testcase/test_login.py的完整代码可以照前文的写法组合起来,先加载 Excel 数据,再用parametrize驱动,最后加上 Allure 装饰器和请求响应 attach。这样写出来的用例,数据和代码分离,新增一条用例只需在 Excel 里添加一行。
6.3 让项目在 CI 里稳定跑起来
项目能在本地跑通只是第一步,要让它成为团队资产,需要接入 CI 流水线。最重要的一件事是保证执行的幂等性:同一个用例集在流水线里跑一百次,结果应该是稳定一致的。要做到这一点,关键在初始数据准备和清理。
我常用的方案是:针对每个测试模块,在文件里造数据并记录关联 ID,测试结束后调用清理接口或直接清库。比如新增用户后记录user_id,模块用例跑完统一删除。这样即使某个用例失败,也只会影响单次执行,不会污染后续用例。
另外要把 HTML 报告和 Allure 报告同时保留。Allure 报告用于全面分析,HTML 报告作为轻量存档,二者不是二选一。CI 集成时的流水线步骤大致如下:
python -m pytest testcase/ -s -v --alluredir=./allure-results --clean-alluredir allure generate ./allure-results -o ./allure-report --clean7. 真实项目里踩过的三个高频坑
框架搭建只是开始,真正烧时间的往往是那些看起来不起眼的坑。下面这三个坑在我的项目里反复出现,也是从网络热词里能看到的大家集中搜索的高频问题,单独拎出来说一下。
7.1 429 too many requests:限流与重试策略
接口自动化跑到后面,一定会撞上一个状态码:429 Too Many Requests。服务器为了保证稳定性,会对单位时间内的请求数设置上限,自动化跑批的时候很容易触发。
requests 库默认情况下收到 429 不会自动重试,用例直接失败,而且失败原因在报告里看起来像服务器错误。我在处理这个问题时的做法是:在 Session 上配置 Retry 策略,把 429 和其他可重试的 5xx 状态码都放进status_forcelist,同时配合退避因子,让重试过程有节奏地拉开间隔。
前文create_session里的配置已经包含这个逻辑。需要强调的是backoff_factor的作用:它让每次重试之间的等待时间指数增长,而不是直接撞成一个“重试风暴”。如果只是简单地把total设为 5,代码可能在 1 秒内重试 5 次,服务端限流反而更严重。实测下来,接口自动化项目的重试次数设为 3、backoff_factor 设为 1,是一个比较平衡的配置。
另外还要区分“可重试的 429”和“不可重试的 429”:如果响应头里带了Retry-After字段,说明服务器明确告诉你多久后再试,这种可以读取这个头做精准等待。
import time import requests def request_with_retry_after(url, **kwargs): retry_times = 3 for i in range(retry_times): resp = requests.request("GET", url, **kwargs) if resp.status_code == 429 and i < retry_times - 1: wait = int(resp.headers.get("Retry-After", 2)) time.sleep(wait) continue return resp return resp7.2 Excel 数据的编码、日期与空行陷阱
Excel 数据驱动虽然好用,但坑也密集。第一个坑是中文乱码。如果用 pandas 的read_excel或者 openpyxl 读取,一般不会乱码,但有人习惯先把 Excel 另存为 csv 再处理,csv 文件的编码问题就会暴露。解决办法很简单:不要走 csv 中间层,直接用 openpyxl 读原文件。
第二个坑是日期格式。Excel 的日期单元格在 openpyxl 里读取后会变成datetime.datetime对象,直接用字符串拼接请求体会报错。处理方式是在读取数据时做一层类型转换:
from datetime import datetime, date def serialize_cell(value): if isinstance(value, (datetime, date)): return value.strftime("%Y-%m-%d %H:%M:%S") return value在load_test_data里对每个单元格套一层serialize_cell就行。
第三个坑是空行和合并单元格。空行往往是因为“看起来空白实际有格式”的行,如果不跳过会生成一堆空的用例;合并单元格在某些场景下会只保留左上角的值。所以在读取方法里,判断首列(比如 case_id)为空就直接跳过这行,是最稳妥的做法。
7.3 用例隔离:token 传递和执行顺序
接口用例之间天然有依赖:登录之后才能获取用户信息,下单之后才能查询订单。但这种依赖如果处理不当,就会变成“用例必须按特定顺序执行”,一旦乱序执行就全盘失败。
我的经验是:不要依赖用例执行顺序,要依赖 fixture 的数据准备。token 获取放在 conftest 的 fixture 里,每个需要登录态的用例都显式声明这个 fixture,然后让 pytest 自己决定用例收集顺序。这样即使随机打乱用例,每个用例在开始前都会先走一遍 fixture 获取 token,不会出现“因为前面的用例没跑所以后面的用例失败了”这种问题。
如果用例依赖的业务数据需要前置创建,也放到 fixture 里:
@pytest.fixture() def create_order(login_token): order_id = api_create_order(login_token) yield order_id api_delete_order(login_token, order_id)yield之前的代码是前置准备,yield之后是后置清理。即使用例断言失败,后置清理代码也会执行,不会把脏数据留到下一次运行。这一点是 pytest 的 fixture 机制非常优雅的地方,unittest 的 tearDown 做不到这种“前后置动作和数据共享一体”的清爽感。
最后再说一个执行顺序经验:如果项目里确实有部分接口依赖上一条用例的响应数据(比如登录后拿 cookie,之后所有接口都依赖 cookie),这种情况应该把“登录获取 cookie”放到 session 级 fixture 里,而不是让测试用例去依赖某一条用例先执行。现在改用 fixture 之后,我的用例文件里几乎不需要插件去控制顺序了。
回过头来看,这套“Python + Requests + Pytest + Excel + Allure”的框架在每一个环节都不是“唯一正确解”,但它们是组合起来最稳的选项。真正让自动化框架活下来的,从来不是某个组件多先进,而是数据和代码是否彻底分离、报告是否让所有人看得懂、用例失败了能不能快速定位。我个人在经历了工具替换的折腾之后最大的体会是:把 Requests 用到底、把 Pytest 的 fixture 吃透、把 Excel 数据设计好、给 Allure 配好层级,这四件事做好,已经能解决绝大多数团队 80% 的接口自动化问题。