接口测试:页面好好的,不代表接口是对的
这是 Harness 系列的 skill 拆解篇,拆的这一份叫 api-testing。它在评测侧干活,是评测阶段
里评判者手边的一件工具:当某个任务或需求在技术方案里定下了接口契约,评判者就用它去验那个接口。母篇《从能跑到可交付:一套前端 AI 协作系统的设计笔记
》讲整套系统的形状,这个系列逐份拆 skill:为什么有它,具体怎么干。我用的系统是开源的 frontend-harness-cli
,文中所有规则都能在仓库里找到对应实现。
页面好好的,接口是坏的
先看一个具体的场景。
一个列表页,接口在改字段的时候把序列化弄坏了:只要列表里有数据,接口就返回 500。前端写得挺规范——请求失败就走 catch,catch 里渲染空状态。于是页面打开,干干净净一个"暂无数据",配上插画,看起来就是一份合法的空列表。
评测的人打开页面,截图,页面没毛病;功能清单逐条对,列表能打开、空状态有、加载动画有。全过。
问题是:真实用户那边的列表里不是没数据,是接口根本没把数据送出来。500 被前端的容错吞掉了,吞得非常体面——体面到从页面上完全看不出来。
这一类故障有个共同点:它不住在页面里,住在页面背后。 你把页面验得再仔细,也验不到它。得有人绕到接口那一侧,亲手发一个请求,看它到底回了什么。
这一份 skill 是什么
api-testing 的自我定义写在 skill 开头,一共两句:
从外部用真实调用检查契约。开发者的报告不是证据;你自己的请求才是。
它验的是一个功能所依赖的接口或数据层行为,在评测阶段使用,前提是技术方案(tech-design.md)里给这个功能写了接口契约。输入有三样:
tech-design.md的「API / 接口契约」一节——期望的请求与响应长什么样,以它为准;- 依赖这个接口的
prd.md需求和e2e-cases.json用例——知道验的这些接口到底在给哪些功能供血; - 正在运行的应用或接口,按技术方案里写的方式启动——没有真在跑的服务,这份 skill 无从下手。
它产出的不是一句"接口没问题",而是一份逐条带记录的探针日志(存放在 .harness/tasks/<feature>/evaluation/api-<cycle>.md),被相关 judge 条目的 artifacts 引用,成为评测打分时可以复核的证据。
它怎么干:从契约列用例,逐条真打
流程分五步,没有一步可以省。
第一步,按契约列用例。 不是想起来什么测什么,是把契约展开成一张清单:成功用例、契约里声明的每一种错误、边界(空值、最大长度、分页的边缘页)、无权限、格式错误的输入。一类都不能少——尤其是后两类,专挑没人看着的时候出事的,恰好是这两类。
第二步,每条用例发真实请求。 用 curl、fetch 或 Playwright 的 request 上下文,真的打到正在运行的服务上。这一条有句硬话:mock 掉的传输层不是证据。 mock 是你告诉它答什么它就答什么,拿它验契约,等于让出题的人自己答卷。
第三步,逐条记录、逐条下结论(记录什么,下一节细说)。
第四步,日志落盘并被引用。 日志存到上面那个路径,文件名带 cycle——第几轮评测,就有第几轮的日志;然后在它所支撑的 judge 条目里把这份日志填进 artifacts。证据和分数之间,必须有这条可追溯的线。
第五步,分流两种坏消息。 探出来的问题分两种,处理方式完全不同:
- 契约不符:契约说了,实际没做到。这是一条带严重级别的发现(finding),记进评测结论,功能要为此返工;
- 契约缺失:方案压根没说某种错误应该长什么样。这不是代码的罪,是契约有洞——记成一条改进建议(
add-verification),回到澄清侧去补契约,而不是让评判者现场编一个标准出来打分。
每条用例要留下什么
每条用例的记录是固定格式,五项:
- 方法 + 路径;
- 请求体 / 参数;
- 期望的状态码与响应结构;
- 实际的状态码与响应体(截短,只留判断需要的部分);
- 结论:过,还是不过。
为什么要这么细?因为评测报告的规矩是没有证据,不算通过。一句"分页接口验过了,正常",复核的人什么都查不到:打的是哪一页?期望返回几条?实际几条?全凭记录者一张嘴。而上面这五项摆出来,任何人拿着契约对着日志,就能把这条用例重判一遍。
实际响应体要截短,是怕日志被大响应撑爆;但期望和实际必须成对出现——只有实际没有期望,读者不知道你拿什么在比;只有期望没有实际,那就又回到"开发者的报告不是证据"的老问题。
设计一:为什么不读 handler,非要真打
最容易被质疑的一点:接口行为读代码不就知道了吗?handler 函数写得明明白白,返回什么、什么情况报错,读一遍都有。何必真打?
因为读代码读到的是意图,真打打到的才是事实,而两者之间隔着一整条链路:路由有没有挂上、中间件有没有拦、鉴权配置对不对、序列化有没有把字段弄丢、环境变量指向的是哪个库——这些都不在 handler 函数里,但每一个都能让真实响应和代码意图对不上。开头那个 500,handler 代码本身可能一点没错,坏的是它下游的序列化配置。
这和评测阶段的大原则是同一件事:评判者不能拿被测者的自述当证据。handler 代码就是接口的"自述"。请求打过去、响应拿回来,这是接口在评测当下、当着评判者的面做的事,做不了假。
还有一条配套的独立性规则:不许拿开发者的脚本当证明。 开发者写过自测脚本,你顺手跑一遍全绿——绿的是他的用例集合,他没覆盖的那类请求,恰好可能是坏的那类。用自己的请求、用例从契约重新列,这是这份 skill 保持独立的方式。另外一条:不许为了让用例通过,去改产品代码或数据夹具。 探针是来测量的,不是来把被测对象修到能过的——你动手改了,这轮测到的就不是交付的那份代码了。
设计二:错误长什么样,也是契约的一部分
这份 skill 花在错误上的笔墨,比花在成功上的多:已声明的每种错误要逐条打,边界、无权限、畸形输入一个不少。
原因是前端的错误状态,是照着契约里的错误形状盖出来的。方案里写"参数不合法时返回 422,响应体带 code 和 message",前端就按这个形状写表单报错的渲染。真实接口要是回了一个别的形状——比如框架默认的 HTML 错误页,或者字段名对不上——前端拿不到它等的东西,轻则错误提示出不来,重则渲染当场报错。成功路径对、失败路径形状不对,对用户来说就是"一提交就白屏",和接口全挂没有区别。
所以"错误长什么样"不是边角料,它是契约的一半。另一半的道理在第五步里:如果方案没写错误形状,那是契约的洞,要补契约,不能让评判者凭感觉判——凭感觉判出来的分数,下一轮换个人就变了。
设计三:探针必须现打,旧录像不算证据
日志文件名里的 <cycle> 不是装饰:每一轮评测,探针重新打一遍,生成这一轮自己的日志。
为什么不能拿上一轮的日志来用?因为上一轮的日志证明的是"上一轮那个版本、那份数据下,接口是那样响应的"。代码改过了、夹具变过了,旧响应什么都证明不了。接口测试如果接受录像回放,它就退化成了另一种自述——只不过自述者从开发者换成了历史。
这和整套系统对证据新鲜度的态度一致:截图要当场截,探针要当场打。证据的价值一半在内容,一半在"它就是这一轮、这一版代码产生的"。
它的边界
把边界说清。
第一,API 用例只证明 API,绝不证明浏览器旅程。 这是 skill 里明文写的规则。接口全对,页面照样可能把数据接错地方、把状态绑错组件——那是端到端测试的地盘,不在这份 skill 的射程里。反过来也一样:页面看起来对,证明不了接口对,开头那个场景就是。
第二,它验的是"实现符不符契约",不是"契约对不对"。 方案把响应形状定错了,探针会忠实地把错误形状验成通过。它能发现的是契约的洞(缺失)和实现的偏(不符),判不了契约本身的对错——那是澄清阶段和人的事。
第三,它不修任何东西。 它只产出证据和发现;契约不符回实现阶段修,契约缺失回澄清侧补。探针自己既不改代码,也不改夹具。
下一篇拆 deterministic-eval:评测里还有一类检查,既不是命令的退出码、也不是一次接口响应能定案的——那类判断怎么做,这份 skill 管的是它们。