Python:接口测试、数据校验和测试数据准备怎么组织更顺手

接口测试里最容易出现的误判是:看到请求发出去了、断言也写了,就以为测试已经成型了。

真正把一套接口测试写顺之后,最花时间的通常不是 requests.post(),而是这三件事:

  • 测试数据怎么准备
  • 响应和副作用怎么校验
  • 失败后证据怎么留下来

如果这三块没有组织好,脚本很快就会出现下面这些问题:

  • 同一个订单号在多个用例之间互相污染
  • 断言到处手写,改字段很痛苦
  • 出错时只能看到 assert False
  • 本地偶尔能过,换环境就不稳定

这一篇直接围绕一个实际功能展开:做一个订单接口测试工具

这个工具要完成这些事:

  1. 准备订单创建请求数据
  2. 调用创建订单接口
  3. 校验 HTTP、业务字段和关键数据结构
  4. 保存失败时的请求和响应证据

用这个场景把三块高频内容串起来:

  1. 测试数据准备怎么避免互相污染
  2. 数据校验怎么拆,后面才不会越写越乱
  3. 接口测试结果怎么留证据,排查才不会只靠猜

一、这篇文章要解决什么问题

读完这一篇,应该能独立完成这些动作:

  • 为接口测试准备一套可复用的请求数据
  • 拆出 HTTP 校验、业务校验、结构校验三层断言
  • 让每条用例的测试数据彼此隔离
  • 失败时输出请求体、响应体和关键字段
  • 识别几类典型坏味道,例如直接复用同一份 dict、断言全写在测试函数里

如果这些动作能独立做出来,接口测试就不再只是“能发请求”,而是开始具备真正可维护的测试骨架。

二、先看要完成的实际功能

假设现在要测试一个创建订单接口:

1
POST /api/orders

请求体大概像这样:

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"])

输出:

1
0

这里的问题不是 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
99.8

对接口测试来说,最基本的一层稳定性,就是每条用例的输入数据要彼此独立。

五、数据准备最好拆成“基础数据 + 局部覆盖”

如果每个场景都手写一份请求体,测试文件会很快充满重复代码。

更顺手的做法通常是:

  • 先准备一份基础数据
  • 再按场景覆盖局部字段

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"]

这短期没问题,但用例一多就会变成:

  • 看不出每条断言在校验什么层
  • 某个字段一改,到处一起改

更顺手的拆法通常是三层:

  1. HTTP 校验
  2. 业务校验
  3. 数据结构校验

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
KeyError: '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

如果失败,只能看到:

1
AssertionError

而实际排查最先想知道的是:

  • 请求体是不是错了
  • 接口到底返回了什么

所以接口测试一旦开始跑真实环境,留证据就不是可选项了。

八、把数据准备、校验和留证据真正串起来

现在把前面的内容合成一个完整测试骨架。

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

十一、怎么测试这一层是不是真的掌握了

这一层不能只看懂,要自己改一遍。

可以直接做这些动作:

  1. 增加一个金额为 0 的失败场景
  2. 增加一个 items=[] 的失败场景
  3. assert_order_created() 再拆成更细的结构校验
  4. 把失败证据写到文件而不是只打印到屏幕
  5. build_order_payload()assert_business_ok() 补最小测试

如果这些改动都能独立完成,说明接口测试、数据校验和测试数据准备这三块已经开始真正串起来了。

十二、一个实际排错场景

这类接口测试里一个非常常见的实际问题是:本地跑通过,但 CI 上偶尔失败,而且失败点总不固定。

这时排查顺序通常很直接:

  1. 先看测试数据是不是每次都重新生成
  2. 再看是不是某条用例改了共享 payload
  3. 再看失败时的请求体和响应体有没有留下来

如果最后发现某条失败场景写成了:

1
2
payload = BASE_PAYLOAD
payload["amount"] = 0

那根因通常就不是接口不稳定,而是测试数据被污染了。

十三、一个实际练习

可以直接把这一篇变成一个完整练习。

练习目标:做一个“用户注册接口测试工具”。

要求:

  1. 准备一份基础请求数据
  2. 至少覆盖 3 个场景
  3. 把 HTTP 校验、业务校验、结构校验拆开
  4. 失败时输出请求和响应证据
  5. 确保每条用例的数据互不污染
  6. 至少补一个实际错误场景,例如共享 dict 导致数据串了

如果这个练习能独立做完,说明接口测试组织这一层已经开始真正掌握。

十四、这篇文章学完以后,下一步应该补什么

如果这一篇已经能跟着做完,下一步最适合继续补的是:

  1. pytest 的 fixture、mock、monkeypatch 和参数化测试分别适合解决什么问题
  2. 单元测试里夹具设计怎么兼顾可读性和复用性
  3. 数据处理脚本和接口工具继续变大时,测试目录怎么继续拆

因为到这一步,已经开始进入“测试代码本身也要工程化”的阶段了。

十五、结语

接口测试真正顺手之后,最核心的通常不是请求发得多快,而是这三层有没有立住:

  • 输入数据能不能稳定准备
  • 校验逻辑有没有分层
  • 失败证据有没有留下

只要这三层先稳定下来,接口测试就不容易再变成一堆复制粘贴的请求脚本。