做接口测试这行,从最早用postman手动点点点,到后来用requests写“一次性脚本”,再到后来接触正式的项目级API自动化,我发现最让人头疼的阶段不是接口本身有多复杂,而是脚本越写越多之后,维护成本直接爆炸。header字段改动一下,十几个地方要跟着改;登录态、token、用户id这些前置依赖,每个脚本里都有一套自己的处理方式;断言逻辑更是各写各的,失败的时候打印的信息七零八落。这些痛点逼着我重新设计了一套基于Python的接口关键字封装方案,简单说就是把“发请求”和“写用例”彻底拆开,让请求动作变成可配置的关键字指令,让用例变成结构化数据。这篇文章就把这套方案完整拆开来讲,从架构思路到代码实现,再到落地之后才能遇见的那些坑,全部记录下来。
1. 裸脚本接口测试的三大痛点
1.1 最初的“一发入魂”写法
我最早写接口测试脚本,属于典型的“怎么简单怎么写”。一个登录接口,直接requests.post(url, json=payload),拿到response之后打印一下,再判断resp.status_code == 200,完事。这种写法在接口数量少的时候是真的香,没有多余抽象,逻辑直接,调通一个接口就是一个函数。
python复制import requests
def test_login():
resp = requests.post(
"http://demo.test.com/api/login",
json={"username": "admin", "password": "123456"}
)
data = resp.json()
assert resp.status_code == 200
assert data["code"] == 0
assert data["data"]["token"] is not None
问题在于,这只是一个接口。当接口数量从1个变成10个、30个、80个之后,这种“舒服”就会迅速变成负担。你会发现所有脚本都长得很像,但每个又有一点不一样:有人用requests.get,有人用session.post,有人超时时间不设,有人设了3秒,有人设了30秒。每个人都觉得自己写得很合理,但这些“合理”凑在一起,就变成了一个无法收敛的接口测试工程。
我记忆中最崩溃的一次,是上线环境更换了网关域名,需要把测试脚本里所有http://demo.test.com改成一个新域名,我手动替换了二十多个文件,结果还是有漏网的,跑完测试才发现有几个用例还在请求旧地址。那一刻我就意识到,裸脚本的方式已经不适合了。
1.2 从5个用例到50个用例,问题集中爆发
用例数量过了50条之后,有几个问题会集中冒出来,而且每一个都能单独让人加班:
第一个是请求层逻辑不统一。有的接口需要加签名参数,有的需要带token header,有的body必须按特定格式处理。同样是POST请求,在不同脚本里的写法可能差出好几个版本,排查问题的时候光是对代码逻辑就要花不少时间。
第二个是前后置数据依赖全靠手工。比如下单接口必须先登录拿到token,token过期要刷新,这些逻辑如果每个用例各自处理一遍,既浪费时间又容易出现“这个用例跑通了,那个用例又登录失败”的诡异局面。前期我甚至见过有人为了快速调到接口,直接在脚本里写死一个token,等token一过期,所有用例集体红色。
第三个是断言逻辑没法沉淀。刚开始断言都只是assert data["code"] == 0,但接口测试做到后面,需要断言的场景太多了:字段等于某个值、字段包含某个子串、数组长度大于0、返回时间在某个范围内、数据库里有对应记录……如果每个用例都用原生的assert去拼,整个脚本就是一团无规则堆叠。
我当时的感受是:接口测试脚本本身不是难点,真正难的是怎么让几十上百个用例稳定、可维护、可排查。
1.3 关键字封装到底想解决什么
很多人一听“关键字封装”,第一反应是想到了商业UI自动化测试工具里的“关键字驱动”,觉得是不是要搞一套特别重型的框架。其实放到接口测试里,关键字封装的核心逻辑特别朴素:把接口测试中所有重复的、底层的动作,统一抽象成若干个固定的“关键字”;测试人员编写用例时,只需要按固定格式填写这些关键字的参数,不需要关心底层是怎么实现的。
这个方向解决的核心问题有三个:
- 可复用:登录、加签、断言、提取变量这些动作,都只实现一次,所有用例共享同一份实现。
- 可配置:用例从代码变成结构化数据(我这边用的是YAML,你用Excel、JSON也一样),非开发背景的测试同事也能上手编写。
- 可排查:所有请求、响应、断言结果统一定义处理逻辑,日志输出格式一致,出问题能快速定位。
2. 整体架构拆解:请求层、关键字层、用例层各自管什么
2.1 三层结构设计
我实践的这套封装,整体上分成三层,每一层只关注自己的事情,边界很清晰。
底层是请求层。这一层只做一件事:把HTTP请求的能力收拢成一个统一的方法。不管你是GET、POST、PUT、DELETE,不管你传的是JSON、form表单、文件还是URL参数,这一层统一处理。调用方只需要传入方法和参数,返回的是一个统一封装的响应对象。requests库本身的能力很强,但正因为强,用起来才有可能“百花齐放”,请求层就是要把这些可能性收敛到一条路上。
中间层是关键字层。这一层定义了几个固定的动作,比如request(发请求)、assert(断言)、extract(提取变量)、db(查数据库)、sleep(等待)、log(打印信息)。每一个关键字都对应一个实现函数,入参是结构化数据,出参是执行结果。关键字层不关心你测的是哪个项目,它只负责按指令做动作。
最上面是用例层。这一层就是一份一份的用例文件,用YAML或者JSON写清楚“我要依次执行哪些关键字”。用例层贴近业务,写的是人话,不接触任何代码细节。
text复制用例层(YAML / Excel / JSON)
↓
关键字层(request / assert / extract / db ...)
↓
请求层(统一request方法,封装requests)
↓
requests库 + HTTP协议
2.2 关键字和测试用例的关系
用一个真实的登录+查询用户信息场景来举例,你就能直观感受到这个结构是怎么运转的。
假设有一个接口测试任务,需要先登录拿token,再带token去查用户信息。用裸脚本的方式写,登录脚本和查询脚本是两段独立的代码;用关键字封装的方式写,用例文件变成下面这样:
yaml复制- name: 登录获取token
action: request
method: POST
url: /api/login
body:
username: admin
password: "123456"
extract:
- name: token
path: $.data.token
validate:
- eq: ["$.code", 0]
- name: 查询用户信息
action: request
method: GET
url: /api/user/info
headers:
Authorization: "Bearer ${{token}}"
validate:
- eq: ["$.code", 0]
- regex: ["$.data.email", ".*@test.com"]
这份文件从头到尾没有出现import requests,没有手动去拼header,没有到处写assert。执行引擎会读入这份YAML,逐条执行关键字指令。第一步发登录请求,断言返回码,然后把data.token存到上下文里的token变量;第二步发查询请求,自动把${{token}}替换成真正拿到的token值,再对响应做断言。
这就是关键字封装的直观效果:测试人员写的是“用例”,不是“代码”。
2.3 为什么不是直接用pytest框架硬写
有人可能会问,pytest本身已经很成熟了,fixture、断言、参数化都很好用,为什么还要自己封装一层?
我的观点是:pytest很好,但它解决的是“测试执行框架”的问题,而不是“接口测试用例组织”的问题。你完全可以用pytest去执行关键字用例,事实上我也是这么做的——执行引擎跑完用例之后,把结果汇总到一个TestResult里,再用pytest的pytest_generate_tests或者自定义的hook把每个关键字步骤映射成一条测试用例。这样既能拿到pytes
