前言
很多接口自动化项目,最开始都长得很像:
-
一个
pytest测试文件 -
几个
requests请求 -
几个硬编码 URL
-
再加上一些临时拼出来的断言
一开始能跑,但到了真实项目里,问题很快就会暴露出来:
-
接口数量一多,重复代码迅速膨胀
-
登录态分散在每条用例里,维护很痛苦
-
环境切换、租户切换、账号切换全都容易出错
-
用例虽然能执行,但别人接手时根本看不懂
-
一旦接口路径或参数结构变动,修复成本很高
所以,接口自动化真正需要的不是"把请求发出去",而是搭一套能长期维护的测试工程。
这篇文章我就结合当前这个项目,讲清楚它为什么要这样分层,以及每一层在解决什么问题。
一、这个项目适合用什么思路来理解
这个仓库不是单纯的"接口集合",它更像一套面向企业内部系统的测试基础设施。
从目录上看,核心能力分布在几个层次:
-
client/:处理 HTTP 请求 -
common/:配置、鉴权、断言、日志、数据库、变量存储 -
services/:业务封装层 -
fixtures/:pytest 注入层 -
config/:环境配置和接口路径定义 -
testcases/:只写业务场景的测试层
这种结构最重要的价值不是"看起来整齐",而是让每一层只做自己该做的事。
举个最直观的例子:
如果某个接口的路径改了,理想情况下你只需要改 config/api_path/*.yaml。 如果某个系统的 token 生成逻辑变了,应该只改 common/auth_manager.py。 如果某个业务接口参数变了,优先改 services/,而不是一口气去改几十条 testcases/。
这就是分层的意义。
二、为什么接口自动化不能只靠 requests + pytest
很多人第一次做接口自动化,都会有一种直觉:反正接口测试就是发 HTTP 请求嘛,那我直接写脚本就行了。
这个想法在最初是成立的,但只适合下面这种场景:
-
接口少
-
测试目的简单
-
没有复杂鉴权
-
不需要多人协作
-
不需要长期维护
而企业项目通常不是这样。真实环境里会有这些问题:
1)接口是变动的
路径、参数、header、响应结构都可能调整。
如果请求逻辑和测试逻辑绑死在一起,任何变化都会让用例大面积重写。
2)登录是复杂的
你的项目里就不是简单拿一个 token,而是涉及:
-
SSO 登录
-
code 交换
-
不同业务系统的token 获取
-
不同角色账号切换
-
不同业务系统头部信息注入
3)测试不只是"成功就行"
接口自动化真正关心的是:
-
返回值对不对
-
业务数据是否正确
-
数据库是否落库
-
不同角色是否看到不同数据
-
场景链路是否完整
所以,脚本化写法很快就会到天花板。要想长期做下去,就必须工程化。
三、技术栈选型背后的原因
项目里核心依赖并不复杂:
-
pytest:测试组织与执行 -
requests:HTTP 请求 -
PyYAML:配置和接口定义 -
allure-pytest:报告展示 -
jsonpath-ng:字段提取 -
deepdiff/jsonschema:结构和内容断言 -
pymysql:数据库校验 -
python-dotenv:环境变量加载
这些库都很常规,但关键不在于"用了什么库",而在于它们怎么组合。
比如:
-
pytest负责生命周期、参数化、fixture 注入 -
requests.Session负责连接复用和 header 复用 -
YAML负责把接口定义从代码中剥离 -
allure负责结果可视化 -
jsonpath负责复杂响应结构提取 -
db_helper负责做接口与数据库的最终一致性校验
换句话说,技术栈本身并不"高级",真正高级的是分工清晰。
四、项目分层为什么这么拆
1)client/:只负责 HTTP 能力
client/base_client.py 做的是最底层的请求能力包装:
-
管理
requests.Session -
拼接
base_url -
合并请求头
-
打日志
-
脱敏
-
统一异常处理
它不应该知道"这是库存接口还是客户接口",它只关心"怎么发请求最稳"。
2)services/:只负责业务表达
services/业务系统A/inventory_service.py 里封装的是业务动作:
-
查库存列表
-
查商品属性
-
查商品标签
这一层最重要的是把技术语言翻译成业务语言。
3)fixtures/:只负责把对象注入测试
测试用例不应该自己到处 new client、自己登录、自己手工取 token。
这些动作都应该由 fixture 统一完成。
4)common/:只负责通用能力
配置、鉴权、断言、数据库、变量存储,这些能力都是跨业务复用的,所以统一放在 common/。
5)testcases/:只写业务验证逻辑
这是最终给测试工程师和业务方看的层。
它应该短、清晰、语义明确,而不是一堆"HTTP 细节堆砌"。
五、从一次请求看整个调用链
如果看一个真实用例,比如:
response = 业务系统A_inventory_service.list_inventory()
这个调用背后并不只是"发了一个 POST 请求"。
它实际经过了这样的链路:
-
testcase触发业务方法 -
service从 YAML 读取接口定义 -
service处理默认参数和覆盖参数 -
client拼接请求头和地址 -
auth注入 token 或业务头 -
requests.Session真正发起请求 -
assertion对响应结果统一校验
如果没有这样的分层,一次简单调用背后会变成:
-
到处是 URL
-
到处是 header
-
到处是 token
-
到处是重复断言
一旦维护周期变长,问题就会非常明显。
六、为什么 ConfigManager 是这个项目的核心之一
在很多项目里,配置文件往往只是用来放账号密码。但这个项目里的 ConfigManager 不是这么简单。
它做了两件很重要的事:
1)环境配置统一加载
通过 .env 和 TEST_ENV 控制当前环境,读取 config/test.yaml、config/uat.yaml 等文件。
这样做的好处是:
-
测试环境切换更简单
-
凭据和基础 URL 不需要硬编码
-
本地跑、CI 跑、UAT 跑都可以复用同一套逻辑
2)API 定义统一加载
它会扫描 config/api_path/*.yaml,把多个系统的接口目录合并到同一个可查询的字典里。
这比把接口路径散落在代码里强很多,因为:
-
新增接口时可以只补 YAML
-
接口方法变更时更容易统一维护
-
业务层调用时只需要记住
system.module.api的 key
这个设计非常适合企业内部多系统场景。
七、为什么 Service 层比直接写请求更值得保留
如果测试用例里直接写请求,通常会变成这样:
requests.post(url, headers=headers, json=payload)
问题在于,这种写法无法沉淀。
而 Service 层的作用,是把这种低层细节包装成业务方法。
比如库存服务层:
def list_inventory(self, page_num=None, page_size=None, overrides=None, *, assert_response=True, **kwargs): ...
这个方法不是"为了多一层而多一层",而是为了让调用方表达更清楚的意图:
-
我想查库存列表
-
我可能想改分页参数
-
我可能想临时覆盖条件
-
我默认希望自动断言
这就比单纯传 URL、method、payload 更符合测试工程的使用习惯。
八、fixtures 为什么在这个项目里特别重要
fixtures/api_fixtures.py 是整个项目非常核心的一环。
因为它把"测试前准备"的逻辑从用例里剥离出来了。
例如:
-
sso_token:负责拿默认用户 token -
operator_sso_token:负责拿操作员 token -
业务系统A_client:注入已登录的业务客户端 -
业务系统A_inventory_service:注入库存服务对象 -
业务系统B_client:注入 业务系统B的客户端
这样一来,测试用例根本不需要关心:
-
token 从哪里来
-
登录流程怎么走
-
客户端怎么初始化
-
头部怎么拼
用例只需要关心"我今天要验证什么业务结果"。
这正是 pytest 的 fixture 体系最适合做的事情。
九、这个项目的架构原则是什么
如果我把这个项目的设计原则总结成几句话,会是这样:
1)配置驱动,而不是硬编码驱动
接口定义、环境配置、测试数据尽量外置。
2)业务表达优先,而不是请求表达优先
用例里写"查商品""查客户",而不是写一长串 URL。
3)公共能力集中管理
鉴权、断言、日志、数据库不要散落在各处。
4)默认流程统一,特殊场景可覆盖
常规用例走默认参数,特殊场景通过 overrides 处理。
5)让用例保持可读性
一个好用例应该是别人不用看实现,也能理解它在测什么。
十、小结
这篇文章讲的不是"怎么发一个 HTTP 请求",而是"怎么把接口自动化项目真正做成工程"。
这个项目的核心价值在于:
-
请求能力统一了
-
配置管理统一了
-
鉴权统一了
-
断言统一了
-
用例表达方式也统一了
这会让后续的所有测试场景都站在同一套基础设施上。
补充:不是所有模块都需要抽象多一个service层,而是要看业务复杂度,服务层不是必须的,它是"当接口调用开始带业务语义和复用成本时"才值得抽出来的抽象。
- 如果这个模块的接口调用逻辑,写在用例里 10 行就讲完
- 且 后面几乎不会复用
- 且 参数、鉴权、断言都很简单
那就没必要强行再抽一层 service。
反过来,如果出现下面任意一种情况,就值得抽:
- 同一组接口会被多个用例反复调用
- 一个业务动作要串多个底层接口
- 默认参数、覆盖参数、路径参数替换比较多
- 断言标准统一但又允许局部覆盖
- 后续接口变更频繁,怕改坏很多用例