简介:这是一份基于 python+pytest+yaml+ddt+allure 技术栈的接口自动化测试框架源代码,主要面向需要开展接口测试与自动化转型的测试工程师。框架源自码尚 VIP 课程学习成果,经公司实际项目改造升级后实测可用,使用者只需录入自己项目的接口用例数据即可快速运行,既适合企业内部测试提效,也适合作为自学进阶的落地参考。压缩包内含 126 个文件,类型涵盖 Python 脚本、YAML 用例配置、JSON 数据文件,以及 allure 报告所需的 JS/CSS/HTML 等静态资源,可支撑数据驱动执行、用例管理与测试报告输出;另附接口自动化测试 yaml 用例规范文档,便于团队统一编写格式。资源包整体约 1.02MB,轻量、易部署。目前已有 4341 人学习下载。借助这套框架,读者可以深入理解 pytest 收集机制、ddt 数据驱动思路与 allure 报告集成方式,同时借鉴来自真实项目的目录结构与排错经验,显著降低从零搭建接口自动化框架的时间成本。框架内还包含实用的用例模板、配置示例与目录组织方式,allure 报告模块已按项目实践调通,可直观呈现执行结果与失败原因,便于团队协作排查问题。
1. 为什么是 python + pytest + yaml + ddt + allure:接口自动化的一个务实组合
接手某公司的接口回归任务时,我落地的框架就是标题里这套:python 写逻辑,pytest 管用例收集与夹具,yaml 存接口数据,ddt 做数据驱动,allure 出报告。当时的现场很典型:几百个接口靠人对着 Postman 手动点,一轮回归要一整天,还不敢保证每个参数都覆盖到。这套组合解决的问题很具体——写用例的人不用碰代码,改接口的人不用翻代码,报告里能直接定位是参数问题还是服务问题。如果你正要搭接口自动化框架,或者现在用的是 unittest 加一堆 if-else 拼出来的脚本,这篇能帮你少走一段弯路。
2. 先把框架分层想清楚:pytest 钩子、yaml 数据边界和目录结构
标题里的几个组件不是硬拼在一起的,它们各自负责一个层级:pytest 管执行和夹具,yaml 管测试数据,ddt 或参数化管数据展开,allure 管结果展示。我接到需求后的第一个动作不是写用例,而是先把分层和目录定死。框架不分层,用例超过五十条就会变成黑匣子:改一条数据牵扯一堆代码,加一个接口要复制粘贴两个文件,最后没人敢动这个框架。
2.1 pytest 的 fixture 与钩子函数:比 unittest 强在哪
接口自动化里 pytest 相对 unittest 的核心优势有三个:fixture 的作用域复用、参数化机制、钩子函数。unittest 的套路是 setUp/tearDown 在每个测试方法前后执行,跨用例共享的初始化(比如登录 token)得自己写类变量或者模块级缓存,代码像拼积木。pytest 的 fixture 通过 scope 声明生命周期,一次 session 级的登录 fixture 能被整个项目共享,fixture 里 return 任意对象,用例直接当参数接收。
fixture 的 scope 是第一个要理解透的参数。我把常用作用域整理成了一张表,给团队新来的测试同事看:
| scope | 执行时机 | 适合放什么 |
|---|---|---|
| session | pytest 进程启动后只执行一次 | 登录 token、全局配置、requests.Session |
| class | 每个测试类执行前一次 | 类级别共享的数据,比如一批测试账号 |
| function | 每条测试用例执行前一次 | 独立参数、临时数据、mock 开关 |
不要一上来什么都用 session,session 级 fixture 一旦内部出错,影响的是整个进程的所有用例。我一般只在确定"这个对象可以全局复用"时才升到 session,比如 token 和连接池,其他一律 function 级,保证用例之间的隔离性。
fixture 还有个容易被忽略的功能:yield 之后的部分会在作用域结束时执行,相当于自带清理逻辑。比如建了一个临时订单,用例跑完自动发起删除请求,不用写 tearDown 也不怕用例中途抛异常导致清理逻辑被跳过。这在接口自动化里非常实用,因为造数据是常态,清数据往往被忘掉。
再往下的钩子函数是 pytest 比 unittest 高级的地方。pytest_collection_modifyitems 可以在收集阶段按你的规则调整用例顺序;pytest_addoption 可以加命令行参数,比如 --env 切换环境;pytest_runtest_makereport 能在每条用例结束后拿到 pass/fail 结果,失败时补附件。这三个钩子分别对应第 4 章的报告快照和第 5 章的环境切换。unittest 要做同样的事只能改框架源码,pytest 只要在 conftest.py 里写同名函数。
最后是参数化。unittest 的 subTest 在报告里看不出第几条数据挂了,而且断言失败后还会继续跑后面的数据,掩盖连锁问题;pytest.mark.parametrize 把每条数据展开成独立用例,失败一条不影响其他,报告里还能用 ids 显示中文标题。这一块的完整写法在第 3 章。
2.2 yaml 存什么、代码里写什么:数据驱动的边界
数据驱动这个词被很多文章讲虚了,落地时最常见的误区是"把所有东西都塞进 yaml"。我见过一个项目把断言用的 Python 表达式也写进 yaml,最后数据文件里全是 eval,比代码还难读。我的边界划分很简单:
- yaml 只放跟业务相关、测试人员需要维护的东西:接口路径、请求方法、headers、params、body、预期状态码、预期关键字段;
- 代码里放跟逻辑相关、需要调试的东西:请求发送封装、接口鉴权、断言方法、日志、重试。
判断标准是:换一个新来的测试同学,她打开 yaml 能不能直接改一条用例;换一个接口类型,打开代码能不能只做小改动就适配。如果两边模糊,比如 yaml 里出现了 base_url、token、环境变量的占位,那就是放错位置了。base_url 属于环境配置,token 属于运行期数据,都不该写进用例数据文件。
另外一个边界是数据文件的生命周期。登录、下单这类单接口用例用 yaml 数据驱动没问题;跨接口的流程用例(先登录拿 token、带 token 下单、再查订单)天然是代码里的 fixture 或步骤。硬拆进 yaml 会变成伪数据驱动:yaml 里写一行注释表示 token 需先登录获取,执行逻辑全在代码里,这种拆法维护成本反而更高。
2.3 一版可扩展的目录结构:从 config 到 report
按上面的边界,我一般会搭这样一版目录:
api_test/ ├── config/ │ ├── __init__.py │ └── settings.py # 环境配置聚合,读取 env.yaml ├── data/ │ ├── __init__.py │ └── login.yaml # 每个业务模块一个 yaml 数据文件 ├── common/ │ ├── __init__.py │ ├── read_yaml.py # yaml 读取封装 │ ├── request.py # requests.Session 封装与 token 管理 │ ├── assert_util.py # 断言工具 │ └── log.py # 日志配置 ├── testcases/ │ ├── __init__.py │ ├── conftest.py # 全局 fixture 和钩子函数 │ ├── test_login.py │ └── test_order.py ├── report/ │ ├── results/ # allure 原始结果目录 │ └── html/ # allure 生成的 HTML 报告 ├── requirements.txt └── pytest.ini几个关键约定。config/settings.py 只聚合环境配置,不写任何测试数据;data 和 testcases 平级,因为同一份登录数据可能被登录模块和全流程模块同时引用;common 里的代码禁止 import testcases 下的任何东西,否则会出现循环依赖,改一个文件带崩一片;report 目录进 .gitignore,results 和 html 每次执行重生成。
testcases/conftest.py 是整个项目的核心配置点,session 级 token fixture、allure 失败快照钩子、环境参数注册都放这里。以后团队扩大,可以在 testcases 下按业务建子目录,每个子目录再放自己的 conftest.py,pytest 会从项目根逐级加载 fixture,子目录的同名 fixture 可以覆盖全局逻辑,不会互相污染。
这个目录不是标准答案,它是经过几个项目验证的底线:数据、逻辑、报告三层分离,新人进来基本不用问太多就能找到要改的文件。后面所有代码都按这个结构组织。
3. 从零跑通最小框架:yaml 读取、ddt 数据驱动和断言日志封装
分层定好之后就可以动手了。这一章的目标很明确:装好依赖,写一个读取 yaml 的工具,写一组数据驱动用例,把断言和日志做成公共能力。跑通这一版,框架的骨架就立住了。
3.1 依赖安装与 pytest.ini:版本别乱装
先用一份 requirements.txt 固定依赖。以下是我在一套 Python 3.11 环境里验证过的组合,版本号仅供参考,按你实际环境的稳定版本锁定:
pytest>=7.0 allure-pytest>=2.13 PyYAML>=6.0 requests>=2.31 ddt>=1.6 pytest-rerunfailures>=11.0安装阶段有一个非常典型的翻车点:allure-pytest 和 pytest-allure-adaptor 是两个完全不同的包。后者是很多年前针对 pytest 2.x 写的插件,早就停止维护,装上之后 pytest 启动直接报插件冲突。认准 allure-pytest 这个包名。ddt 这个库本身没有复杂依赖,但它不是 pytest 项目的必需品,装它只有一种情况:团队里有历史遗留的 unittest 风格测试类不想重写。选型细节在 3.3 节展开。
pip install -r requirements.txt项目根目录放 pytest.ini,这是 pytest 的配置入口:
[pytest] testpaths = testcases addopts = -v -s --alluredir=report/results --clean-allurediraddopts 里的 --alluredir 指定 allure 原始结果的输出目录,--clean-alluredir 在每次执行前清空旧结果,避免上一次失败运行残留的半截 JSON 混进新报告。testpaths 限定 pytest 只收集 testcases 目录,否则 common 下的工具函数也可能被当成测试收集进去。
注意:-v 在用例数少于 300 条时建议保留,接口用例的报错信息在终端里多打几行远比只看进度点有用;用例量上来之后再换 -q 配合报告定位。
3.2 封装 yaml 读取层:safe_load 与编码的两个细节
data 目录的读取逻辑放在 common/read_yaml.py,全项目只有这一个文件允许直接操作 yaml:
from pathlib import Path import yaml # 项目根目录:common 的上一级 BASE_DIR = Path(__file__).parent.parent def load_yaml(path: str): """读取 data 目录下的 yaml 文件,返回 Python 对象 path 是相对于项目根目录的路径,例如 data/login.yaml """ file_path = BASE_DIR / path with open(file_path, "r", encoding="utf-8") as f: return yaml.safe_load(f)两个细节值得说透。第一是必须用 yaml.safe_load 而不是 yaml.load。yaml.load 在没有显式 Loader 参数时会用全功能 Loader,允许反序列化任意 Python 对象,数据文件一旦出现意外标签就是隐患。safe_load 只反序列化基本类型,对测试数据完全够用。第二是 open 时固定 encoding="utf-8",不加这个参数,Windows 平台上会按系统默认编码(GBK)去读含中文的 yaml 文件,直接报 UnicodeDecodeError。这是国内团队搭这套框架时最常遇到的第一个报错。
读取时机也要清楚:上面的代码在模块 import 阶段执行,也就是 pytest 收集用例时就完成了 yaml 解析。好处是数据文件语法错误能尽早暴露,坏处是收集阶段崩溃会让整个运行都起不来。所以 yaml 文件结构尽量保持简单,不要用别名、不要写多文档流(多个 --- 分隔),嵌套过深的锚点语法在编辑器校验和报错信息上都不友好。
3.3 用 pytest 参数化实现数据驱动:ddt 库在 pytest 里怎么摆
先看 data/login.yaml。我习惯一个文件放一个业务接口的所有正向、反向用例,每条用例是顶层列表的一个元素:
- case_name: 正确账号密码登录 method: post url: /api/v1/login headers: Content-Type: application/json body: username: admin password: "123456" expected: code: 200 msg: success - case_name: 密码错误 method: post url: /api/v1/login headers: Content-Type: application/json body: username: admin password: wrongpass expected: code: 200 msg: password error注意 password 字段我加了引号。yaml 会自动做类型推断,纯数字不写引号会被解析成 int,传过去服务端大概率拒收,这个坑在第 5 章专门展开。expected 里只放必要的字段,不要整段响应都写成预期,否则接口新增一个返回字段,你的用例就无意义地失败。
pytest 侧的标准写法:
import allure import pytest from common.read_yaml import load_yaml from common.request import http_client # 模块收集时读取一次,参数化的数据源 cases = load_yaml("data/login.yaml") @pytest.mark.parametrize("case", cases, ids=[c.get("case_name") for c in cases]) def test_login(case): with allure.step("发送登录请求"): resp = http_client.send(case) with allure.step("校验响应状态码"): assert resp.status_code == case["expected"]["code"] with allure.step("校验关键字段"): assert resp.json().get("msg") == case["expected"]["msg"]parametrize 会把 yaml 里的每条数据展开成独立用例,ids 参数生成用例标题,在终端和报告里一眼能看出是哪个场景挂了。第二个参数是数据源列表,第三个 ids 必须和数据源长度一致,否则 pytest 会报收集错误。
标题里提到的 ddt 对应的是另一种写法,适用于 unittest 风格的历史测试类:
import ddt import unittest from common.read_yaml import load_yaml cases = load_yaml("data/login.yaml") @ddt.ddt class TestLoginDDT(unittest.TestCase): @ddt.data(*cases) def test_login(self, case): resp = http_client.send(case) self.assertEqual(resp.status_code, case["expected"]["code"])ddt 和 parametrize 都能跑,区别在细节:ddt 展开出来的用例在 pytest 收集器里是 test_login_1、test_login_2 这种自动编号,没有 ids 机制,几乎对应不上 case_name;parametrize 则天然融入 pytest 的生态,allure 报告、重试插件、失败快照全部兼容。
提示:标题里的 ddt 在纯 pytest 项目里我更建议用 parametrize 替代。ddt 官方就是为 unittest.TestCase 设计的,硬套在 pytest 原生测试类上会有收集行为不一致的问题。我一般只在迁移存量 unittest 用例、想最小化改动时保留 ddt。
3.4 断言与日志封装:排错时最值钱的资产
接口自动化的断言不能写成清一色的 assert resp.json() == expected。响应里的时间戳、traceId、随机字符串每次都在变,整段对比等于给自己埋雷。我按三层来封装断言。
第一层是状态码,只能说明 HTTP 层通了;第二层是关键字段,把业务 code 和核心字段取出来对比,封装在 common/assert_util.py:
def assert_contains(resp_json: dict, expected: dict): """校验 expected 中的每个 key 都存在且值相等""" for key, value in expected.items(): assert key in resp_json, f"响应缺少字段 {key}" assert resp_json[key] == value, f"字段 {key} 期望 {value},实际 {resp_json[key]}"第三层是格式校验,适合校验订单号、手机号这类动态值,比如断言 order_no 是否匹配 ^ORD\d{12}$,避免服务端返回空字符串也被当成成功。
日志是接口自动化里最容易被省掉、排错时又最值钱的部分。common/log.py 里做一次基础配置:
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s %(name)s [%(levelname)s] %(message)s", ) logger = logging.getLogger("api_test")在请求封装里用 logger.info 打印请求方法和响应摘要,而不是打印完整响应体。完整响应体在用例多的时候会刷爆终端,而且敏感信息可能泄露。打印状态码、耗时和响应里的一两个业务字段就够了,完整内容留给 allure 附件,这一点在第 4 章展开。
4. 集成 allure:把执行结果变成能交付的测试报告
框架跑通只是第一步,报告才是让团队愿意用起来的动力。allure 集成到位之后,测试结果可以直接扔给开发和产品看,不用再解释"这个红的是不是环境挂了"。
4.1 allure-pytest 插件与 allure 命令行:先分清这两个东西
allure 在实际落地时要装两个独立的组件:pytest 插件和 allure 命令行工具。allure-pytest 是 pip 包,负责在测试执行过程中把用例结果、步骤、附件写进 report/results 目录,生成的是 JSON 格式的原始结果;allure 命令行是一个单独的 Java 程序,负责把这个 results 目录渲染成 HTML 报告。只装插件不装命令行,跑完测试你会发现 results 下全是 JSON,根本没有可看的报告。
命令行工具装好之后,生成报告的固定命令是:
allure generate report/results -o report/html --clean allure open report/htmlgenerate 把 results 聚合到 html 目录,--clean 先清空旧报告;open 会起一个本地服务并在浏览器打开。持续集成场景下不需要 open,generate 完之后把 html 目录作为构建产物归档就行。还有一个 allure generate --single-file 参数会生成单文件报告,方便直接发出去,但用例多、附件多的时候单文件会非常大,酌情使用。
pytest.ini 里已经配了 --alluredir,所以执行 pytest 之后 results 目录会自动生成。一个容易忽略的点:如果手动删除过 results 目录,记得先创建它,否则 allure-pytest 会报目录不存在。本地反复调试时,--clean-alluredir 会清掉旧结果,这是符合预期的,别以为是数据丢了。
4.2 用例标注、动态标题和步骤附件:报告不再是一堆函数名
默认情况下 allure 报告里的用例名是 test_login[0]、test_login[1] 这种格式,人肉对应 case_name 很痛苦。通过 allure 的标注体系可以把报告变得可读。在 3.3 节的用例基础上补全:
import allure import pytest from common.read_yaml import load_yaml from common.request import http_client cases = load_yaml("data/login.yaml") @allure.feature("账号体系") @allure.story("登录") @pytest.mark.parametrize("case", cases, ids=[c.get("case_name") for c in cases]) def test_login(case): # 运行期覆盖参数化标题,否则报告里只有 test_login[0] allure.dynamic.title(case.get("case_name")) with allure.step("发送登录请求"): resp = http_client.send(case) with allure.step("校验响应状态码"): assert resp.status_code == case["expected"]["code"] with allure.step("校验关键字段"): assert resp.json().get("msg") == case["expected"]["msg"] allure.attach(resp.text, "登录接口响应", allure.attachment_type.TEXT)feature 和 story 是报告里的两级分类,feature 对应业务模块,story 对应功能点。dynamic.title 在运行时把用例标题改成 yaml 里的 case_name,这一步是关键——ids 参数在参数化展开时就固定了,没法引用运行时数据,所以在函数体内用 dynamic 覆盖。with allure.step 把用例拆成步骤,报告里展开能看到"发送请求"和"校验响应"两个子步骤,失败时直接定位到失败在哪一步。
attach 是给用例挂附件的入口,TEXT、JSON、HTML 都支持。接口自动化里最实用的附件是响应文本和完整请求体。注意 attach 要在断言执行之前把 resp 存好,否则断言抛异常时后面的行不会执行,失败现场就丢了。更稳妥的做法是把响应快照放在 4.3 节的失败钩子里统一处理。
4.3 失败自动快照:用 pytest_runtest_makereport 生成可定位的失败现场
用例断言失败时,仅凭 traceback 在接口自动化里往往不够:你看不到请求发了什么、服务端回了什么。我在 conftest.py 里用 pytest_runtest_makereport 钩子统一打快照,这样就不用在每条用例里手动写 attach:
import allure import pytest from common.request import http_client @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): # 先执行用例,结束后才能拿到 report 对象 outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: last_resp = http_client.last_response if last_resp is not None: allure.attach(last_resp.text, "失败响应体", allure.attachment_type.TEXT)这段代码的思路是:钩子在每条用例执行完后触发,通过 report.failed 判断是否失败,然后从 common.request 里的全局 http_client 读取最近一次请求的响应,挂到失败用例上。前提是请求封装里每次 send 都更新 last_response,即使断言失败这个值也不会丢。这样报告里每个失败用例都有完整响应体,不需要用例本身写任何 attach 代码。
注意:快照不要做得太重。请求摘要、响应体、状态码三样足够定位绝大多数接口问题。如果某些接口响应达到几 MB,全部挂上去会让报告打开很慢,可以在快照里截断,只保留前 2000 个字符。
5. 这套框架的 5 个常见坑:从 yaml 类型转换到 token 失效的排查路径
这一章的每条坑都是我在实际项目里踩过或者帮团队排过的。每条按现象、原因、解决三步写,遇到类似问题可以直接照着排查。
5.1 yaml 类型自动转换:密码 123456 为什么变成了数字
现象:yaml 里写的登录密码是 123456,请求发出去服务端一直报密码格式错误,代码里 print 出来发现 password 的值是 int 而不是字符串。
原因:yaml 规范自带类型推断。纯数字解析成 int,形如 0123 的会被按八进制解析,true/false/yes/no 会被解析成布尔值。测试数据一旦被转换,接口收到的就是完全不同的类型。
解决:给可能产生歧义的字符串加引号,写成 password: "123456" 是标准做法。更保险的是在 read_yaml 封装里对读取结果做递归类型检查,把 int 自动转 str,但这样会把本来希望是数字的字段也转掉,所以按字段加引号更可控。另外带前导零的字符串,比如 "001" 不写引号会变成整数 1,这种掉得无声无息,排查起来最费时间。
5.2 ddt 与 pytest 参数化重复执行:用例跑了两遍
现象:测试类上加了 @ddt.ddt,方法上又用了 @pytest.mark.parametrize,跑 pytest 发现同一条用例的日志出现两次。
原因:ddt 在类加载阶段通过包装init生成新的测试方法(test_login_1、test_login_2),pytest 的参数化是另一套独立的展开机制,两套叠加等于数据被展开两遍。更隐蔽的是另一个方向:用了 @ddt.data 但类上没加 @ddt.ddt,或者类不继承 unittest.TestCase,ddt 不会报错但数据完全没有展开,用例只跑一次,你以为覆盖了多条数据实际只跑了一条。
解决:二选一。新代码一律用 pytest.mark.parametrize;存量 unittest 类用 ddt 就不要再加 parametrize。如果测试类继承 unittest.TestCase,pytest 会把整类按 unittest 模式收集,ddt 展开的方法名是 test_login_1 这种,报告可读性差,长期维护建议逐步迁移到 parametrize。
5.3 token 过期导致批量失败:session 级 fixture 的续期方案
现象:用例在一开始全过,跑到第 30 条左右开始集中出现 401,单独重跑挂掉的那条又是正常的。
原因:接口鉴权 token 有有效期。如果每条用例都重新登录取 token,要么频繁刷新触发单点限流,要么拿到的是刚过期的一批 token;如果多条用例并发执行时同时刷新 token,还会出现用 A 的 token 跑 B 用例的串号问题。
解决:token 放在 session 级 fixture 里,整个 pytest 进程只取一次,再在请求封装里做自动续期——请求返回 401 时用账号重新登录,更新全局 token 后重放当前请求。续期逻辑必须加锁,防止两条用例同时续期产生竞争。具体实现见第 6 章。
5.4 allure 报告乱码与附件打不开:编码和清理问题
现象:allure 报告打开后,用例名或步骤里的中文显示成乱码;附件里的 JSON 双击打开是乱码。
原因:乱码大多出在执行环境——yaml 或代码文件读取时没有显式指定编码,Windows 下走了 GBK;或者 allure 命令行渲染时用的系统默认编码不是 UTF-8。附件打不开则多为 results 目录残留,上一次运行写到一半的 JSON 文件没被清掉,allure generate 解析到半截文件直接跳过甚至卡住。
解决:所有涉及文件读写的代码统一传 encoding="utf-8",包括 open、yaml、csv 读取。执行命令固定为 pytest --alluredir=report/results --clean-alluredir,生成报告固定为 allure generate report/results -o report/html --clean,两处 --clean 一个清结果一个清报告。如果还乱码,检查 allure 命令行工具版本,旧版本对中文字体的渲染有已知问题,升级到较新版本即可。
5.5 环境切换时 base_url 漂移:数据文件里不该写 host
现象:同一套用例在测试环境全过,切到预发环境跑,大量用例 ConnectionError。排查发现 yaml 里接口地址写的是带具体域名的完整 URL,换环境只换了测试工具的 host,框架里没换。
原因:把环境相关的信息写进了用例数据。base_url 属于环境配置,不属于测试意图,yaml 一旦包含完整 URL,就做不到一套数据跑多套环境。
解决:yaml 里只写相对路径,比如 url: /api/v1/login,请求封装里用全局配置的 base_url 拼接。环境切换通过命令行参数实现,conftest.py 里用 pytest_addoption 注册 --env,settings 模块读取后拼进请求地址。这样同一个 yaml 可以在测试、预发、生产三套环境跑,只需要指定 --env。
6. 进阶:把 token 会话、失败重试和轮询断言做成公共能力
框架跑顺之后,稳定性就是下一步要解决的。这一章把 5.3 的 token 续期、偶发网络失败重试、异步接口轮询断言沉淀成公共层,避免每个业务用例各写一套。
6.1 conftest.py 里维护 session 级 token 与 requests.Session
token 的获取放在 conftest.py 的 session 级 fixture 里,续期逻辑收进 common/request.py:
# testcases/conftest.py import pytest from common.request import http_client @pytest.fixture(scope="session") def login_token(): # 整个 pytest 进程只触发一次登录 return http_client.ensure_token()# common/request.py import requests import threading class HttpClient: def __init__(self): self.session = requests.Session() self.lock = threading.Lock() self._token = None def ensure_token(self): if self._token is not None: return self._token with self.lock: if self._token is None: # 账号信息从 config.settings 读取,这里不硬编码 resp = self.session.post(LOGIN_URL, json=LOGIN_BODY) self._token = resp.json()["token"] return self._tokenensure_token 的双重判断加锁,保证并发用例同时调用时只触发一次登录。requests.Session 会复用连接池,几十条用例共用一个 session,比每条用例新建 requests 实例省掉大量 TCP 握手。注意 Session 对象不是线程安全的,多进程执行时每个进程要单独实例化一个 HttpClient,不能序列化共享。
6.2 请求级重试与轮询断言:接口自动化最后的稳定性拼图
接口自动化最容易被偶发失败搞崩。网络抖动、服务启动中的瞬时 502、异步任务还没就绪就断言,都会让凌晨的定时任务跑挂。请求级重试要对着状态码做,而不是盲目重试一切失败:
def send_with_retry(self, case, retries=3, timeout=10): """只对网关类状态码重试,4xx 属于业务决策错误不重试""" url = BASE_URL + case["url"] for attempt in range(retries): resp = self.session.request(case["method"], url, headers=case.get("headers"), json=case.get("body"), timeout=timeout) if resp.status_code not in {502, 503, 504}: return resp return resp轮询断言适用于订单状态、支付回调这类异步场景:发送请求后,轮询查询接口直到目标状态出现或超时,再把断言放在轮询循环外部。把这两个能力做成公共方法,而不是每个用例自己写循环,是框架走向稳定的关键。我吃过一次教训:把重试逻辑写在业务用例里,结果每条用例都有一段 while True,日志混乱、超时不可控,最后花两个晚上重构回公共层。公共能力越早沉淀,框架的使用成本越低。希望帮到你。
本文还有配套的精品资源,点击获取