ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

“可信的创造”专题 · 第 1 篇

“可信的创造”专题 · 第 1 篇

# 测试全绿,功能没做:五种“假完成”

AI 编码工具从补全进入能改多个文件、能跑命令的阶段后,“测试通过”越来越难直接等于“完成”。代码能跑、测试全绿,为什么还不能算完成?这篇文章拆开五种常见的“假完成”:假实现、重复实现、未接入、能力退化、无主交付。每种都给出识别信号、最小检查动作和一条边界,最后附一份评审前能直接用的清单。文章适合正在把 AI 引入真实项目的工程师和技术负责人。

先看一段核心实现:

```js

// summary.js

export function summarizeOrder(order) {

return { total: 0, currency: "CNY", itemCount: 0 };

}

```

它接收订单,返回汇总,字段齐全。测试只检查了形状:

```js

assert.equal(typeof result.total, "number");

assert.equal(typeof result.currency, "string");

assert.equal(typeof result.itemCount, "number");

```

测试通过。问题藏在这段绿色里:函数没有读 `order`。买两件还是一百件,`total` 和 `itemCount` 都是 0。测试证明了“返回值里有这三个字段,类型也对”,没有证明金额算对了。

一个返回空壳的函数可以满足形状,一个算错金额的函数也可以。假完成就是这种状态:产出在表面上满足完成信号,实际在一个或多个完成条件上落空。

## 五种落空方式

把“完成”拆开,至少有四件事要成立:代码真的做事;同一能力只实现一次;真实入口能走到它;替换旧路径时没有削弱旧能力。再加上验收有可复跑的证据、有人对交付负责,才接近“可信完成”。

按问题离开单个文件的程度,可以识别出五种落空方式:假实现、重复实现、未接入、能力退化、无主交付。前四种在代码和系统里,第五种在交付和人身上。

这五种失效不是 AI 才带来的。AI 可能改变它们的频率和发现难度,但这里没有证明频率,也不声称哪一种更常见。

## 一、假实现:字段齐全,行为缺席

回到开头的函数。它的问题不在代码长短,而在函数名承诺了汇总订单,代码却没有读订单。空方法、常量返回、只处理正常输入、用日志代替行为、用待办标记充当实现,都属于这一类。更隐蔽的一种是“看起来有逻辑”:只在一个分支里正确,只在测试数据上正确。

判断时先看契约。接口的默认实现、可选钩子的空实现、序列化载体、测试替身,本来就不承诺行为。契约要求行为,代码却没有行为,才是假实现。行数不是证据,行为才是。

读实现时,先找输入和输出之间的关系:`items` 有没有被遍历,`price` 和 `quantity` 有没有参与计算,空数组会返回什么。找不到这些关系,函数再整齐,也只是把返回值的形状摆对了。

成本较低的检查是负控:故意制造一个小缺陷,看测试会不会失败。把返回值改成常量,把条件改成永远为真,把写入操作删掉,然后跑测试。测试还是绿的,说明断言没有碰到行为;测试变红,至少说明这条断言看见了被破坏的行为。

## 二、重复实现:两套规则都通过,合起来对不上

假实现是一个函数不做事;重复实现是两个函数都在做事,而且各自都有测试。

一个常见场景是金额格式化。订单页有一套函数,处理舍入、负数和空值。新增报表模块时,生成过程又写了一套语义相近的函数:一套按分四舍五入,一套先截断到角;一套拒绝负数,一套把负数当成退款。两边各有测试,正常样例输出一致。上线后,客服发现同一笔退款在两个页面差了几毛。

问题不在于哪一套“写错了”。两个函数各自在测试里都说得通,问题在于同一个能力有了两套规则。一套改了,另一套没改;一套处理了负数,另一套没有。两套代码都通过测试,合在一起却产生了第三个结果。

检查时,按能力盘点,不按文件盘点。拉一次调用图,看有没有两条并行路径;用同一组输入跑两套实现,比较返回值、错误行为和副作用;最后问一句:另一套什么时候删除?对比的输入要落在可能分歧的地方,正常值只能证明两边都能跑,边界值、非法值和错误路径才更容易暴露规则差异。

也有合理的重复:迁移期双写、两个边界清楚的业务领域各自保留模型、性能特化路径。合理的重复有一个共同点:边界清楚,负责人明确,退出条件写下来。

## 三、未接入:模块有覆盖,主流程没有

未接入让前两种失败都失去意义:代码再真、再唯一,用户走不到,就等于没做。

一个典型场景:退款原因统计已经合入主分支,单元测试全绿,覆盖率报告也很漂亮。上线三天,报表里这一列始终是空的。查下去,新模块只被两个测试文件引用,真正的退款入口还在调用旧函数。

模块覆盖率和入口覆盖率是两回事。模块覆盖率回答“测试有没有执行这些行”,入口覆盖率回答“真实请求有没有走到这些行”。前者可以很高,后者可以是零。

检查时,从入口开始:加一条从生产入口到产出的测试,查生产入口的可达性,查谁使用这个产出,核对开关的默认值和打开条件。还有一个简单的思想实验:把测试文件里的导入删掉,生产代码会少掉什么?如果什么都不会少,说明这段代码可能只活在测试里。

分阶段发布时,代码先合入、开关后打开,是合理的。合理的未接入有一个条件:有人知道它现在没接,为什么没接,什么时候接。只写“以后打开”,等于没有计划。

## 四、能力退化:替换之后,旧能力悄悄没了

能力退化发生在真实请求已经走到新代码之后。系统没有报错,只给出错的结果。

一个金额解析函数上线后,财务对账发现一笔负数退款。旧实现会拒绝负数和非数字输入;新实现更短,只做了一次类型转换。正常金额解析正确,测试通过。上线后,负数被当成合法金额,账对不上。

新实现可能写得没问题;问题是它没有保留旧实现的防御。替换旧路径时,旧代码里的参数检查、边界条件、错误处理和副作用,可能在重写中消失。测试没有发现,因为测试只覆盖了正常输入。更隐蔽的一种情况是:旧测试跟着旧代码一起被删掉了。

核心动作是对比。对新旧实现跑同一组输入,包括非法值和边界值;比较返回值、错误行为、副作用和调用顺序;再做一次负控,删掉检查分支,看测试会不会失败。旧代码是行为的说明书。替换之前,先把旧行为列出来,比读新代码更重要。

有一种退化是故意的,叫简化。这种简化可以有,但要有证据:契约改了,调用方改了,错误处理改了,批准记录也在。简化不是退化。退化是没人决定,能力就悄悄没了。

## 五、无主交付:代码合了,没有人接电话

前四种失败发生在代码和系统里。第五种发生在交付和人身上。

一次交付评审会上,有人问:“这个功能上线后,出故障谁接电话?”回答是一个团队名。再问谁批准上线、谁负责回滚、看哪个指标,会议室里没有人能给出名字。代码合入主分支,任务关闭,看板一片绿色。

几天后的凌晨,监控报警。值班工程师看到一个陌生的指标,不知道该找谁核实。他先把开关关掉,因为这是唯一能做的动作。关掉之后,谁来判断什么时候能打开,也没有人知道。

问题出在完成定义:它只要求合并,没有要求交付。合并是一个动作,交付是一个过程。合并之后还有验收、上线、监控、回滚和后续维护。只要这些环节没有具名负责人,代码就算合入了,也不能算交付了。

检查可以从一张交付责任卡开始:谁批准上线、谁接故障电话、怎么回滚、看哪个指标。然后做一次回滚演练,记录谁执行、花了多久、卡在哪。团队名可以承担协作,不能承担判断。

## 一个小实验

一组小实验覆盖了三种可以用代码模拟的失败:假实现、未接入、能力退化。实验有三组缺陷样本:一个返回常量的订单汇总、一个只被测试调用的折扣模块、一个丢掉输入验证的金额解析函数。

三组合计八条测试:四条弱测试,四条强测试。弱测试只看形状、只看模块、只看正常输入;强测试看具体值、从入口走、覆盖边界。缺陷样本先跑一轮:四条弱测试通过,四条强测试失败。补上修复实现后再跑一轮:十三条测试,九条通过,四条失败。

这个结果说明的是断言的作用,不是 AI 的比例。它说明测试通过只能证明写下的断言通过;断言没有碰到行为,假实现、未接入和能力退化就会穿过去。它不能说明 AI 生成的代码里有多少假完成。实验环境是 Node.js 20,代码是人工构造的。

## 识别清单

- [ ] 假实现:这个函数用了输入的哪些字段?如果返回常量,测试会失败吗?断言里有没有具体的值,而不只是类型?

- [ ] 重复实现:这个能力在别处是否已经存在?哪一套是权威?另一套什么时候删除?

- [ ] 未接入:从生产入口走到这段代码的路径是什么?开关默认值是什么?谁使用这个产出?

- [ ] 能力退化:旧实现防住了哪些输入?新实现是否也防住了?旧测试去哪了?

- [ ] 无主交付:谁批准上线?谁负责回滚?出故障时谁被呼叫?

- [ ] 总问题:如果今天上线,我能拿出什么证据证明它完成了?这些证据对应当前这个版本吗?

## 边界

五种模式不是完整分类。权限、并发、数据迁移、外部依赖、性能退化,都可能长出这里没有列出的形态。

每一种模式都有合理的例外。接口默认实现可以是空的;迁移期可以有两套实现;分阶段发布可以先不接线;有意的简化可以删掉旧防御;共享平台可以由一个团队负责。判断的关键在例外本身:有没有边界、负责人和退出条件。

检查也会误伤。一个接口的默认实现被当成假实现,团队补了一条没有意义的测试,最后发现契约本来就允许空实现。误判的代价,是团队开始为了通过检查而写代码,而不是为了解决问题。

检查本身也有成本。对每一次小改动都做五种检查,团队会被拖慢,最后检查可能被绕过。更合理的做法是按风险分配:改动的影响面越大、回滚越难、数据越敏感,证据就应该越强。对于探索性原型、一次性脚本、内部工具,过度检查可能不值得。

最后要警惕一种反效果:把“识别假完成”变成“增加审批”。如果一套检查只让流程变长,却没有让团队更敢做改动,它就失败了。检查的目的,是让“完成”这句话重新可信,从而把时间还给真正的创造。

## 下一次评审,先问哪一句

完成不是代码存在,也不是测试通过。完成是一个可以被检验的判断:功能真实、只实现一次、接入主路径、没有削弱旧能力、有人对交付负责。

五种假完成,就是这句话可能落空的五个位置。

下一次评审,先不要问“测试过了吗”。先问:如果这个实现是假的,哪条证据会失败?如果没有人能指出一条,绿色报告还没有回答完成的问题。

---

依据与边界:文中的实验是 Node.js 20 下的人工构造样本,案例为合成场景,用于说明机制,不用于估计真实项目中的比例。

这是“可信的创造”专题的第 1 篇。下一篇讲:换了三次会话,验收为什么会丢。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进