先说结论,我那1873条用例是被我自己删掉的
前年冬天我接手了一个跑了三年多的接口测试仓库,pytest + requests 写的,1873 个测试函数,Jenkins 每晚 2 点跑一轮,耗时 41 分钟。失败率常年在 12% 到 18% 之间晃。团队当时的做法是:早上来看失败列表,把眼熟的几个跳过,然后红着发版。
刚接手那两周我也是这么干的。直到有一次线上出了个订单金额算错的事故,我回头翻测试报告,发现对应的那条用例当天是红的——被当成环境问题跳过了。那一刻我才意识到:问题不是环境不稳,是这批用例本身写得就有毛病,红得太多,真出问题的那条混在里面根本看不出来。
后来我花了大概六周时间做了一件事:把 1873 条砍到 412 条,重写断言。跑完一轮从 41 分钟掉到 3 分 20 秒,假失败率从 13% 掉到 1.8%。下面是我踩过的坑和最后沉淀下来的做法,不一定对,但至少在我待过的三个团队里都跑通了。
假失败率才是接口测试的第一指标,不是覆盖率
大部分人聊接口测试,第一句话就是覆盖率。我原来也这么想,直到我统计了一次失败原因分布。
那 1873 条用例,一个月的失败记录我拉下来分了类:真正测出 Bug 的失败只占 6.4%,环境/数据问题占 41%,还有 52.6% 是我称之为「断言写死了业务数据」导致的——比如断言 orderId == 10086,但这个订单在测试库里隔三差五被清理。
换句话说,超过一半的红是假红。假红一多,人对红色就脱敏了。这是接口测试最要命的地方,比覆盖率低严重十倍。覆盖率低顶多是漏测,假失败率高是整个测试体系失去信任。
所以我现在衡量一个接口测试项目健不健康,先看三个数:一轮全量跑多久、假失败率多少、最近 30 天有没有人真的因为测试红了去改代码。前两个数好拿,第三个数得问团队里的人,问的时候他们一般会笑一下,那个笑就是答案。
断言分三层,这是我最想讲的部分
我现在的习惯是把每个接口的断言拆成三层,不同层给不同的权重和数量。
| 层级 | 校验内容 | 用例覆盖比例 | 我实测的真问题占比 |
|---|---|---|---|
| 契约层 | HTTP 状态码、Content-Type、JSON Schema、字段类型与必填性 | 100% | 约 68% |
| 业务层 | 业务 code、核心状态流转字段(如 status 从 1 变 2) | 40% | 约 27% |
| 数据层 | 精确 ID、时间戳、金额、分页 total | 5% | 约 5%,且误报最多 |
关键在数据层。我原来每条用例平均 6.3 个断言,其中 4 个是数据层的。删到平均 2.1 个之后(契约层 1 个 Schema 校验、业务层 1-2 个),假失败率直接掉到 2% 以下。
举两个具体的坑。
第一个,code 字段。国内后端很常见,HTTP 状态码永远 200,业务失败靠 body 里的 code 区分,比如 code: 0 成功、code: 50001 参数错。如果你只断言 resp.status_code == 200,那这个接口等于没测。我当时写了个 BaseChecker,统一在契约层里加一条:assert resp.json()['code'] in EXPECTED_CODES,这一条就干掉了大量漏测。
第二个,时间戳精度。同一个项目里,createTime 有接口返回秒级 1704067200,有接口返回毫秒级 1704067200000,还有返回字符串 "2024-01-01 00:00:00" 的。我原来写 assert data['createTime'] > 0,看起来没问题,实际上遇到字符串直接 TypeError。后来统一改成用 JSON Schema 声明 "type": "integer" 或者 "type": "string", "format": "date-time",结构错了立刻报,值是多少不管。
分页接口还有一个经典坑:断言 len(data['list']) == 10。测试库数据被别人删了两条,这条就红了。我现在的写法是断言 0 < len(data['list']) <= pageSize,再加一条 total >= len(data['list'])。听起来松,但它测的是分页逻辑本身有没有坏,而不是测试库现在有几条数据。
测试数据管理,这块的坑比断言还深
我们早期图省事,直接把生产库脱敏后同步到测试库。前三个月挺爽,后来越来越慢,某天一个测试账号的订单被清掉了,连带 28 条用例连环挂。排查花了两个小时,最后发现是另一个同事手动在测试库跑了个清理脚本。
后来改成用例自己造数据、自己清理。规则定得很死:
- 所有造出来的数据带固定前缀,比如
autotest_,方便批量清理和事后排查 - 用
PYTEST_XDIST_WORKER环境变量区分进程,否则-n 8并发跑的时候两个进程会造出同名数据互相踩 - teardown 里清理,但清理失败不打红,只记 warning,避免清理逻辑本身变成噪音源
具体的 conftest 大概长这样:
import os
import pytest
import requests
WORKER = os.environ.get('PYTEST_XDIST_WORKER', 'gw0')
PREFIX = f'autotest_{WORKER}_'
@pytest.fixture(scope='function')
def temp_user(api_client):
username = PREFIX + str(uuid.uuid4())[:8]
resp = api_client.post('/api/user', json={'name': username})
uid = resp.json()['data']['id']
yield uid
api_client.delete(f'/api/user/{uid}')
这里有个细节值得说:scope='function'。我一开始图快写成 scope='module',结果一个模块里 12 条用例共用同一个用户,其中一条改了用户状态,后面 11 条全挂。接口测试的数据隔离粒度,我建议就跟用例走,别省这点时间。
并发和幂等,接口测试里最容易被跳过的一块
功能用例跑绿了不代表没问题。我有一次用 k6 压一个下单接口,100 并发,P99 从 210ms 直接飙到 2.3s。查了半天应用代码没毛病,最后发现是 HikariCP 的 maximumPoolSize 默认值 10,请求全堵在拿连接那一步。这个数字我记了很久,因为它太容易被忽略了——接口测试如果只跑单请求,这类问题永远不会暴露。
幂等测试也是。下单、支付、发消息这类接口,我的做法是同一个幂等键(requestId 或业务单号)连续发两次,断言只生成一条记录。这里有个坑:幂等键放在 header 还是 body,不同团队不一样,测试前一定要问清楚,我就因为这个跟后端吵过一次,最后发现是我把 key 塞错地方了。
我这边的并发用例跑得很少,一个项目大概 8 到 12 条,只覆盖写操作里最核心的那几个接口。跑得太全没意义,而且压测数据清理起来很麻烦。
落到 CI 上,我的分层策略
# 提交前(开发者本机 pre-push)
冒烟集:60 条,目标 90 秒内跑完
# 合并前(MR pipeline)
主链路集:约 400 条,目标 4 分钟内
# 每晚
全量集 + 并发集,目标 8 分钟内
重点是那个 90 秒和 4 分钟,不是我拍脑袋定的。开发者本机跑测试超过 90 秒就会开始跳过,MR 流水线超过 5 分钟就会开始有人点重试。这个是人的耐心阈值,比技术指标重要。
另外一个我想强调的观点:flaky 用例不要加重试。我见过好几个项目配了 pytest-rerunfailures --reruns 3,看着红变绿挺舒服,实际上是把手里的报警器关了。现在我的规矩是,一条用例一周内红了两次且原因不明,要么当周修掉,要么直接删,不允许它躺在那里重试。
最后,接口测试的价值不在覆盖率,在变更命中率
这个观点可能有点反常识。我做过一次统计:某次后端重构改了 6 个 Service 方法,晚上全量跑,1873 条用例里有 903 条被触发执行了(覆盖率看着不错),但真正断言到那 6 个改动点的,只有 14 条。
也就是说,900 条用例跑了,但它们测的东西跟这次改动毫无关系。这就是所谓的覆盖率虚高。现在我更愿意花时间在契约测试上——把接口的 Schema 和关键字段边界固定住,用 300 到 400 条用例覆盖所有接口的结构,反而更容易在改动时抓到问题。
我现在的项目,接口测试 387 条,一轮 2 分 15 秒,最近 30 天假失败率 1.2%。上周有个同事主动跟我说,他因为测试红了去改了一行代码。这句话比任何覆盖率数字都让我踏实。