周一早上九点,CI 又红了。37 个接口用例失败,我端着咖啡回来点了下重跑,全绿。这事我遇到过不下十次,直到有天我把 Jenkins 的原始日志拖下来一行行看——失败的那批用例全打在了同一个测试账号上,而这个账号的余额字段刚好被另一条并发跑的用例扣光了。不是接口有 bug,是我自己的用例在互相抢劫。
从那以后我对「接口测试」这四个字的信任度降了一半,只信两件事:用例之间有没有共享状态,超时是不是显式写死的。
工具选型,大部分人问的是错的问题
「Postman 和 pytest 哪个好」这问题本身回答不了,两者压根不是一层东西。Postman 是给人看的,pytest 是给 CI 看的。你想要一个能截图发群里的调试工具,又要它同时扛起 800 条用例的回归,最后基本是两头不讨好。
我上家团队干过这事:470 条用例全塞在 Postman Collection 里,配 Newman 在 Jenkins 上跑。跑到第 200 条左右,Node 进程内存吃到 1.2G,Collection Runner 偶尔会静默跳过几个请求——不报错,就是没跑,而且断言还是绿的。后来我在 Postman 社区翻到过类似反馈,官方建议拆分成多个 Collection 再跑。另外提一句,Postman 免费版从 2024 年 3 月调整后,Collection Run 的额度被砍到每月两位数(我记得是 25 次),团队真要靠它跑回归,基本得掏钱。
我在 M1 / 16G 的机器上串行跑 500 条查询类用例,粗略耗时大概是这样:
| 方案 | 用例写法 | 500 条串行耗时 | 并行能力 | CI 友好度 |
|---|---|---|---|---|
| Postman + Newman | JS(pm.test) | 4 分 20 秒左右 | Newman 无原生并行,要自己拆 collection 分片 | 中,报告靠 newman-reporter-html |
| pytest + requests + jsonschema | Python | 1 分 50 秒 | pytest-xdist -n auto,8 worker 后约 22 秒 | 高 |
| Karate 1.4 | Gherkin + Java | 40 秒上下(我开 5 线程) | 原生 parallel,一行代码切线程数 | 高,自带 HTML 报告 |
| RestAssured 5.4 | Java given/when/then | 1 分 30 秒 | JUnit5 parallel execution | 中,得额外配 extent report |
别被那张表的耗时骗了。这里是查询类接口,写操作我从来不敢并行,一并行数据就互相污染。速度快的方案不等于适合你,取决于你的接口有没有副作用。
前三个坑,全是细节翻车
坑一:requests 不写 timeout,等于给自己埋雷。 requests 的 timeout 默认是 None,也就是无限等。这跟 httpx 不一样,httpx 默认 5 秒就超时了。我吃过一次亏,某个下游服务 TCP 建连之后不返回,100 个线程全挂在那儿,跑了一晚上。后来统一改成 connect / read 双超时:
import requests
# 别这么写,默认无限等待
# r = requests.get(url)
# 也别只写一个数,读超时和连超时是两码事
# r = requests.get(url, timeout=30)
# 这么写
r = requests.get(url, timeout=(3.05, 15))
为什么是 3.05 而不是 3?这是 AWS 那篇讲超时和重试的经典文章里的建议:Linux 的 TCP SYN 重传是 1s、2s、4s 的退避,连接超时设成略大于 3 秒,才能保证把第二次重传等完,又不至于拖太久。这个数字我从 2019 年用到现在。
坑二:JSON Schema 的 additionalProperties 千万别设成 false。 这是我最想吐槽的一条。很多人写响应校验,第一反应是「字段一个都不能多」,于是加了 additionalProperties: false。结果上游加了个 traceId 字段,你的用例全红。加字段是向后兼容的变更,在消费者契约测试(Pact 那一套)里它就明确不算破坏性变更——真正会挂掉下游的是删字段、改类型、改必填。所以 schema 只校验「我需要的字段在不在、类型对不对」,别管人家多给了什么。
顺带一个版本差异:exclusiveMinimum 在 JSON Schema draft-07 里是布尔值(配合 minimum 用),到了 2020-12 变成了数字。你要是用 jsonschema 这个 Python 库但 schema 里写了 "$schema": "https://json-schema.org/draft/2020-12/schema",得确认库版本支持,不然校验结果会静默不对。
坑三:时区。 数据库存 UTC,接口返回 "createdAt": "2024-03-15T02:30:00Z",前端 new Date(...) 直接渲染成 10:30,运营跑来问「这个用户凌晨两点半下的单?我们那时候没搞活动啊」。接口测试这边对应的坑是:你的断言里写 assert data["createdAt"] == "2024-03-15 10:30:00",本地跑绿,CI 跑红——因为 CI 容器的 TZ 是 UTC。我的做法是断言只校验格式和相对时间(比如 abs(now - createdAt) < 60 秒),绝不硬编码时间字符串。
后四个坑,全是并发和精度
坑四:pytest-xdist 的共享状态。 加了 -n auto 之后用例快了 5 倍,然后开始随机挂。原因是 xdist 默认按文件分发,同一个文件里的用例跑在同一个 worker,但不同文件之间会抢登录态的全局变量。后来改用 --dist loadscope,让同一个 module 的用例落到同一个 worker,再配合 pytest --forked 隔离,才稳下来。另外,凡是带写操作的用例,我直接给它们打了个 serial marker,用 -m "not serial" 过滤掉再并行。
坑五:分页接口的 total 字段。 你写了个用例,断言第一页返回 total == 137。跑一次绿,跑两次红,因为另一个用例往库里插了数据。total 在并发写入场景下本来就不是个稳定值,我的处理是把它当成「范围断言」:assert 100 < total < 1000,或者干脆在测试库里锁表。
坑六:金额别用浮点。 接口传 19.9,服务端 Python 里 float 一算变成 19.899999999999999,序列化出去就是 19.899999999999998。这类 bug 在 UI 上被四舍五入掩盖了,只有在接口层做精确比对才会暴露。我们后来统一改成传字符串形式的金额,或者用 decimal.Decimal 加 str() 序列化。
坑七:别在功能用例里断言响应时间。 assert r.elapsed.total_seconds() < 0.5 这种写法,在本地是稳的,在共享的 CI 机器上,旁边有个同事在跑编译,它就红给你看。性能断言应该放进专门的基准测试里跑,比如 Locust 或者 k6,单独一条流水线,别混进功能回归里制造 flaky。
我的结论:接口断言的价值是有排序的
做了几年下来,我现在判断一条接口用例值不值得写,看的是这条断言在「接口变更时会不会响」。按这个标准排下来,顺序是:契约稳定性 > 错误码语义 > 字段值 > 响应时间。
契约稳定性指的是字段在不在了、类型变没变、必填变没变——这是唯一能让下游真的挂掉的东西,也是最该覆盖的。错误码语义排第二,是因为业务方真的会依赖 code == 40001 这种判断,你改了它就等于改了接口。字段值排第三,因为它最容易变,也最容易被 mock 数据影响,投在上面的维护成本经常超过收益。响应时间垫底,前面说过了。
但现实里我见过的大多数团队,投入是反过来的——花 70% 的精力去断言字段值,剩下 30% 里再砍一半去写性能断言,最后谁也没覆盖契约。
对了,还有个隐藏的问题:你的用例是在测接口,还是在测你自己写的 mock 数据?如果断言里的期望值是从同一个环境里抄来的,那这条用例只是在复述现状,不是在验证需求。我现在写用例的习惯是先看接口文档,把期望值从文档里抄,再用环境里的真实响应去对——对不上,要么文档错了,要么接口错了,两种都得处理。