AI 写的测试可能是假绿灯:三种识别方法

测试全绿但功能是坏的——AI 生成的测试有三种典型假象:断言恒真、把实现抄进断言、为了让测试通过而改测试。这篇给识别方法与三道检查,也说明 AI 在测试上真正强的地方。

让 AI 写测试是我用得最顺手的一件事:格式规整、边界想得比我全、一分钟能出十条用例。但用得久了会发现一个陷阱——测试全绿,功能还是坏的。这篇讲三种"假绿灯"和我的识别方法。

假绿灯之一:断言恒真

AI 生成断言时,最常写出的形式是"我断言它不报错":

func TestParseConfig(t *testing.T) {
    got, err := ParseConfig("testdata/config.yaml")
    if err != nil {
        t.Fatalf("should not error: %v", err)   // 只验证了不报错
    }
    _ = got                                     // 结果根本没检查
}

这个测试能过,但把 ParseConfig 改成"永远返回空结构体"也能过。它测的是"函数不会崩",不是"函数做对了"。

识别方法很土:问自己一句「把实现改错,这个测试会不会红?」。如果不会,它就没有保护价值。

我现在的做法是在提示词里直接约束:

每个测试必须断言具体返回值或副作用(字段值、条数、状态变化),不允许只断言 err == nil。

假绿灯之二:把实现抄进断言

第二种更隐蔽——AI 会把被测函数的逻辑"重写一遍"放进测试里当期望值:

// 被测代码
func Discount(u User) int {
    if u.Vip && u.Years >= 3 { return 30 }
    if u.Vip { return 10 }
    return 0
}

// AI 写的测试(期望值由同一套规则推导)
want := 0
if u.Vip && u.Years >= 3 { want = 30 } else if u.Vip { want = 10 }

这样写,一旦业务规则改了(比如 3 年变 5 年),实现和测试会一起改错,测试永远绿。

正确做法是写死具体的期望值,让它成为一条独立的事实:

cases := []struct{ name string; u User; want int }{
    {"老会员", User{Vip: true, Years: 5}, 30},
    {"新会员", User{Vip: true, Years: 1}, 10},
    {"非会员", User{Vip: false, Years: 9}, 0},
}

假绿灯之三:为了通过而改测试

第三种最危险,因为它看起来像"修好了"。当你把失败告诉 AI,它有相当概率去改断言而不是改实现:

测试失败:TestEscapeFormula 期望 '+8 但得到 +8。 AI:已修改测试期望值为 +8。✅

它没说谎——测试确实通过了。但真实的 bug(Excel 公式注入)被"修"进了测试里,代码反而更危险了。

我的规矩是:测试失败了,只允许改实现;要改断言,必须先说清"原期望为什么是错的"。这句话必须是我来判断的,不是它。

我的三道检查

每次 AI 交来一批测试,我会做三件事:

  1. 反向验证:随机挑两三个测试,手动把实现改错(把 >= 改成 >、把 return 30 改成 return 20),看测试是否变红。不变红的就是无效测试。
  2. 看断言内容:只断言 err == nil、只 t.Log 不断言、断言里出现和实现一样的推导逻辑——全部要求重写。
  3. 看数据来源:期望值必须写成常量或字面量,不能由被测代码或同一个辅助函数算出来。

顺便说说 AI 在测试上真正强的地方

不是说 AI 写测试不好——它有三个地方比我强得多:

  • 穷举边界:0、负数、空字符串、超长字符串、Unicode、时区、闰年,它一次能列全(我经常漏);
  • 模板化:表驱动、t.Run 子测试、并发用例的结构,写得又快又整齐;
  • 补错误路径:让它"把每个 return err 的分支都覆盖一遍",它比我耐心。

所以正确的分工是:让它生成用例清单和骨架,我来定义期望值并做反向验证。

小结

测试的价值不在于"数量"或"覆盖率数字",而在于实现错的时候它会红。AI 生成的测试默认满足不了这个条件——它擅长写"看起来对的测试"。把反向验证这一步补上,假绿灯就会现形;跳过这一步,你得到的只是一份让自己安心的假证据。

返回博客列表