接口测试里最容易出现的误判是:看到请求发出去了、断言也写了,就以为测试已经成型了。
真正把一套接口测试写顺之后,最花时间的通常不是 requests.post(),而是这三件事:
- 测试数据怎么准备
- 响应和副作用怎么校验
- 失败后证据怎么留下来
如果这三块没有组织好,脚本很快就会出现下面这些问题:
- 同一个订单号在多个用例之间互相污染
- 断言到处手写,改字段很痛苦
- 出错时只能看到
assert False
- 本地偶尔能过,换环境就不稳定
这一篇直接围绕一个实际功能展开:做一个订单接口测试工具。
这个工具要完成这些事:
- 准备订单创建请求数据
- 调用创建订单接口
- 校验 HTTP、业务字段和关键数据结构
- 保存失败时的请求和响应证据
用这个场景把三块高频内容串起来:
- 测试数据准备怎么避免互相污染
- 数据校验怎么拆,后面才不会越写越乱
- 接口测试结果怎么留证据,排查才不会只靠猜
一、这篇文章要解决什么问题
读完这一篇,应该能独立完成这些动作:
- 为接口测试准备一套可复用的请求数据
- 拆出 HTTP 校验、业务校验、结构校验三层断言
- 让每条用例的测试数据彼此隔离
- 失败时输出请求体、响应体和关键字段
- 识别几类典型坏味道,例如直接复用同一份 dict、断言全写在测试函数里
如果这些动作能独立做出来,接口测试就不再只是“能发请求”,而是开始具备真正可维护的测试骨架。
二、先看要完成的实际功能
假设现在要测试一个创建订单接口:
请求体大概像这样:
1 2 3 4 5 6 7
| { "user_id": 1001, "amount": 99.8, "items": [ {"sku": "SKU-100", "count": 1} ] }
|
成功响应大概像这样:
1 2 3 4 5 6 7 8
| { "code": 0, "message": "ok", "data": { "order_id": "ORD-20240711-001", "status": "CREATED" } }
|
这篇文章要把下面这条链路写顺:
- 先准备请求数据
- 再调用接口
- 再拆层做校验
- 最后把失败证据写出来
三、先看最容易写乱的版本
很多接口测试第一版通常长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import requests
payload = { "user_id": 1001, "amount": 99.8, "items": [ {"sku": "SKU-100", "count": 1} ] }
resp = requests.post("https://example.com/api/orders", json=payload, timeout=3) data = resp.json()
assert resp.status_code == 200 assert data["code"] == 0 assert data["data"]["status"] == "CREATED"
|
这段代码当然能跑,但很快会碰到:
- 请求体一改,多个用例一起改
- 断言多了以后测试函数开始变长
- 某次失败后不知道到底发了什么数据
- 要测试金额为 0、商品为空、用户不存在时,测试数据会互相污染
这不是“写得不优雅”,而是测试数据和校验逻辑还没开始分层。
四、测试数据准备为什么不能直接共用一份 dict
1. 先抽一个最小工厂函数
先把请求数据准备动作从测试函数里拿出来。
1 2 3 4 5 6 7 8
| def build_order_payload(): return { "user_id": 1001, "amount": 99.8, "items": [ {"sku": "SKU-100", "count": 1} ] }
|
调用:
1 2
| payload = build_order_payload() print(payload)
|
输出:
1
| {'user_id': 1001, 'amount': 99.8, 'items': [{'sku': 'SKU-100', 'count': 1}]}
|
2. 一个实际错误:直接复用全局数据
错误写法:
1 2 3 4 5 6 7 8 9 10 11 12
| BASE_PAYLOAD = { "user_id": 1001, "amount": 99.8, "items": [ {"sku": "SKU-100", "count": 1} ] }
payload = BASE_PAYLOAD payload["amount"] = 0 print(BASE_PAYLOAD["amount"])
|
输出:
这里的问题不是 Python 奇怪,而是:
payload = BASE_PAYLOAD 不是复制
- 两个变量指向的是同一个对象
这在接口测试里特别危险,因为一条用例改了数据,下一条用例可能直接被污染。
3. 实际做法:每次返回一份新数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def build_order_payload(): return { "user_id": 1001, "amount": 99.8, "items": [ {"sku": "SKU-100", "count": 1} ] }
payload = build_order_payload() payload["amount"] = 0
another_payload = build_order_payload() print(another_payload["amount"])
|
输出:
对接口测试来说,最基本的一层稳定性,就是每条用例的输入数据要彼此独立。
五、数据准备最好拆成“基础数据 + 局部覆盖”
如果每个场景都手写一份请求体,测试文件会很快充满重复代码。
更顺手的做法通常是:
1. 先写一个带覆盖参数的工厂
1 2 3 4 5 6 7 8 9 10
| def build_order_payload(**overrides): payload = { "user_id": 1001, "amount": 99.8, "items": [ {"sku": "SKU-100", "count": 1} ] } payload.update(overrides) return payload
|
调用:
1 2
| payload = build_order_payload(amount=0) print(payload)
|
输出:
1
| {'user_id': 1001, 'amount': 0, 'items': [{'sku': 'SKU-100', 'count': 1}]}
|
2. 一个实际错误:浅层 update 不会处理嵌套结构
例如:
1 2
| payload = build_order_payload(items=[]) print(payload)
|
这当然可以。
但如果只是想改 items[0]["count"],直接靠 update() 就不够细了。
这时通常更稳的做法是:
- 要么让工厂函数支持更明确的参数
- 要么在测试里拿到新对象后,针对当前场景单独改
例如:
1 2
| payload = build_order_payload() payload["items"][0]["count"] = 2
|
只要保证每次都是新对象,这样改就不会污染别的测试。
六、接口校验不要全部堆在一个 assert 链里
接口测试最容易写乱的第二块,就是断言。
第一版很容易把所有校验都塞进测试函数里:
1 2 3 4 5
| assert resp.status_code == 200 assert data["code"] == 0 assert data["message"] == "ok" assert data["data"]["status"] == "CREATED" assert "order_id" in data["data"]
|
这短期没问题,但用例一多就会变成:
- 看不出每条断言在校验什么层
- 某个字段一改,到处一起改
更顺手的拆法通常是三层:
- HTTP 校验
- 业务校验
- 数据结构校验
1. 先拆 HTTP 校验
1 2
| def assert_http_ok(response): assert response.status_code == 200
|
2. 再拆业务校验
1 2 3
| def assert_business_ok(data): assert data["code"] == 0 assert data["message"] == "ok"
|
3. 再拆数据结构校验
1 2 3 4
| def assert_order_created(data): assert "data" in data assert "order_id" in data["data"] assert data["data"]["status"] == "CREATED"
|
4. 一个实际错误:失败时只看到 KeyError
错误写法:
1
| assert data["data"]["order_id"]
|
如果接口失败响应根本没有 data 字段,就会先报:
这类报错当然能看出失败,但不够友好。
实际做法是先拆层校验:
1 2
| assert_business_ok(data) assert_order_created(data)
|
这样至少能先知道是业务失败,还是结构缺失。
七、把请求调用和结果留证据也组织起来
如果一条接口测试失败,最有价值的通常不是一句 assert False,而是下面这些证据:
- 发了什么请求体
- 收到了什么响应
- 失败的是哪一层断言
1. 先封装一次请求调用
1 2 3 4 5 6
| def create_order(session, base_url, payload): return session.post( f"{base_url}/api/orders", json=payload, timeout=3, )
|
2. 失败时把证据打出来
1 2 3 4
| def dump_case_context(payload, response): print("request payload:", payload) print("response status:", response.status_code) print("response body:", response.text)
|
3. 一个实际错误:失败后只知道“断言没过”
错误写法:
1
| assert data["code"] == 0
|
如果失败,只能看到:
而实际排查最先想知道的是:
所以接口测试一旦开始跑真实环境,留证据就不是可选项了。
八、把数据准备、校验和留证据真正串起来
现在把前面的内容合成一个完整测试骨架。
test_orders.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| import requests
def build_order_payload(**overrides): payload = { "user_id": 1001, "amount": 99.8, "items": [ {"sku": "SKU-100", "count": 1} ] } payload.update(overrides) return payload
def create_order(session, base_url, payload): return session.post( f"{base_url}/api/orders", json=payload, timeout=3, )
def assert_http_ok(response): assert response.status_code == 200
def assert_business_ok(data): assert data["code"] == 0 assert data["message"] == "ok"
def assert_order_created(data): assert "data" in data assert "order_id" in data["data"] assert data["data"]["status"] == "CREATED"
def dump_case_context(payload, response): print("request payload:", payload) print("response status:", response.status_code) print("response body:", response.text)
def test_create_order_success(): session = requests.Session() base_url = "https://example.com" payload = build_order_payload()
response = create_order(session, base_url, payload) data = response.json()
try: assert_http_ok(response) assert_business_ok(data) assert_order_created(data) except AssertionError: dump_case_context(payload, response) raise
|
这个版本最重要的不是“封装得多”,而是测试链路已经开始清楚了:
- 输入数据怎么准备
- 请求怎么发
- 断言怎么分层
- 出错时证据怎么留
九、再补一层:测试数据文件什么时候值得上场
当场景开始变多时,测试数据可能会从代码里进一步拆出来。
例如:
1 2 3 4 5 6 7 8 9 10 11 12
| { "valid_order": { "user_id": 1001, "amount": 99.8, "items": [{"sku": "SKU-100", "count": 1}] }, "zero_amount_order": { "user_id": 1001, "amount": 0, "items": [{"sku": "SKU-100", "count": 1}] } }
|
但这里有一个边界:
- 场景少的时候,函数工厂更直观
- 场景很多、要复用给多条用例时,数据文件才开始更有价值
不要一上来就把所有数据全搬到 JSON/YAML 文件里,否则调试成本反而更高。
十、完整版本放在一起
下面给出一个更完整的接口测试骨架。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| import requests
def build_order_payload(**overrides): payload = { "user_id": 1001, "amount": 99.8, "items": [ {"sku": "SKU-100", "count": 1} ] } payload.update(overrides) return payload
def create_order(session, base_url, payload): return session.post( f"{base_url}/api/orders", json=payload, timeout=3, )
def assert_http_ok(response): assert response.status_code == 200
def assert_business_ok(data): assert data["code"] == 0 assert data["message"] == "ok"
def assert_order_created(data): assert "data" in data assert "order_id" in data["data"] assert data["data"]["status"] == "CREATED"
def dump_case_context(payload, response): print("request payload:", payload) print("response status:", response.status_code) print("response body:", response.text)
def run_create_order_case(session, base_url, payload): response = create_order(session, base_url, payload) data = response.json()
try: assert_http_ok(response) assert_business_ok(data) assert_order_created(data) except AssertionError: dump_case_context(payload, response) raise
return data
|
十一、怎么测试这一层是不是真的掌握了
这一层不能只看懂,要自己改一遍。
可以直接做这些动作:
- 增加一个金额为
0 的失败场景
- 增加一个
items=[] 的失败场景
- 把
assert_order_created() 再拆成更细的结构校验
- 把失败证据写到文件而不是只打印到屏幕
- 给
build_order_payload() 和 assert_business_ok() 补最小测试
如果这些改动都能独立完成,说明接口测试、数据校验和测试数据准备这三块已经开始真正串起来了。
十二、一个实际排错场景
这类接口测试里一个非常常见的实际问题是:本地跑通过,但 CI 上偶尔失败,而且失败点总不固定。
这时排查顺序通常很直接:
- 先看测试数据是不是每次都重新生成
- 再看是不是某条用例改了共享 payload
- 再看失败时的请求体和响应体有没有留下来
如果最后发现某条失败场景写成了:
1 2
| payload = BASE_PAYLOAD payload["amount"] = 0
|
那根因通常就不是接口不稳定,而是测试数据被污染了。
十三、一个实际练习
可以直接把这一篇变成一个完整练习。
练习目标:做一个“用户注册接口测试工具”。
要求:
- 准备一份基础请求数据
- 至少覆盖 3 个场景
- 把 HTTP 校验、业务校验、结构校验拆开
- 失败时输出请求和响应证据
- 确保每条用例的数据互不污染
- 至少补一个实际错误场景,例如共享 dict 导致数据串了
如果这个练习能独立做完,说明接口测试组织这一层已经开始真正掌握。
十四、这篇文章学完以后,下一步应该补什么
如果这一篇已经能跟着做完,下一步最适合继续补的是:
- pytest 的 fixture、mock、monkeypatch 和参数化测试分别适合解决什么问题
- 单元测试里夹具设计怎么兼顾可读性和复用性
- 数据处理脚本和接口工具继续变大时,测试目录怎么继续拆
因为到这一步,已经开始进入“测试代码本身也要工程化”的阶段了。
十五、结语
接口测试真正顺手之后,最核心的通常不是请求发得多快,而是这三层有没有立住:
- 输入数据能不能稳定准备
- 校验逻辑有没有分层
- 失败证据有没有留下
只要这三层先稳定下来,接口测试就不容易再变成一堆复制粘贴的请求脚本。