AI写测试翻车实录:测试全绿,上线还是崩了——一个format函数暴露的盲区

上篇说完测试策略,我就翻车了

写测试策略那篇的时候,我提到B档让AI写测试骨架、人审断言,翻了一次车。

就是这次。

一个看起来不能再简单的函数------格式化价格,把分转成元,加个货币符号。AI写完了代码,自己写了测试,跑了全绿,我扫了一眼覆盖率100%,点了merge。上线第一天,用户看到一个空白页。

排查下来,问题出在AI写的测试上------或者说,出在我对"AI写测试"这件事的认知上。

现象

线上报错信息:

java 复制代码
TypeError: Cannot read properties of null (reading 'toFixed')
    at formatPrice (utils/formatPrice.ts:4:25)
    at OrderSummary (components/OrderSummary.tsx:42:12)

formatPrice 函数长这样:

typescript 复制代码
// utils/formatPrice.ts
export function formatPrice(cents: number): string {
  return `¥${(cents / 100).toFixed(2)}`
}

调用方是订单摘要组件,从API获取订单数据后逐项渲染价格。某个历史订单的 price 字段在某些条件下是 null------API的设计问题,但前端没有做防御。

排查

我第一反应是"测试覆盖率没跑满",但打开测试文件一看,覆盖率100%,10条测试用例全部通过。

typescript 复制代码
// AI生成的测试 --- 10条全部通过
import { formatPrice } from '../formatPrice'

describe('formatPrice', () => {
  it('formats 1299 cents to ¥12.99', () => {
    expect(formatPrice(1299)).toBe('¥12.99')
  })
  
  it('formats 0 cents to ¥0.00', () => {
    expect(formatPrice(0)).toBe('¥0.00')
  })

  it('formats 99 cents to ¥0.99', () => {
    expect(formatPrice(99)).toBe('¥0.99')
  })

  it('formats 10000 cents to ¥100.00', () => {
    expect(formatPrice(10000)).toBe('¥100.00')
  })

  // ... 还有6条类似的,全是 "正常整数 → 正常字符串"
})
text 复制代码
// 运行结果
PASS  utils/__tests__/formatPrice.test.ts
  ✓ formats 1299 cents to ¥12.99 (2ms)
  ✓ formats 0 cents to ¥0.00 (1ms)
  ✓ formats 99 cents to ¥0.99 (1ms)
  ✓ formats 10000 cents to ¥100.00 (1ms)
  ✓ ... 6 more

Tests: 10 passed, 10 total
Coverage: 100%

所有测试都正常。但仔细看------10条测试用例,全部是"正常输入→正常输出"。没有一条测试 nullundefined、负数、浮点数、超大数的场景。

根因

AI为什么没写这些边界测试?不是它"不会",是它看不到

AI生成测试用例时,输入来源只有两个:函数签名和JSDoc。

php 复制代码
函数签名: formatPrice(cents: number): string
JSDoc:     无(或者简单的 "格式化价格")

基于这两个信息,AI推导出的结论是:cents 是一个 number,所以测试用例应该用 number 类型的数据。它不知道这个 number 可能来自一个不可靠的API,不知道API在某些条件下返回 null,不知道调用方有没有做防御性检查。

AI的测试边界等于它的信息边界。 它看不到函数之外的调用上下文,所以它生成的测试只覆盖了"函数自己能看到的"范围。

这个问题的本质,跟第二弹里AI重构删"死代码"的根因一样------AI的静态分析只能看到代码本身,看不到代码被怎么用。

修复

修复分三步:

第一步:补测试。 把边界情况和异常输入加进去。

typescript 复制代码
// 补的测试
describe('formatPrice --- edge cases', () => {
  it('handles null gracefully', () => {
    // @ts-expect-error --- 测试运行时的实际行为
    expect(() => formatPrice(null)).not.toThrow()
  })

  it('handles undefined gracefully', () => {
    // @ts-expect-error
    expect(() => formatPrice(undefined)).not.toThrow()
  })

  it('handles negative numbers', () => {
    expect(formatPrice(-100)).toBe('¥-1.00')
  })

  it('handles large numbers without overflow', () => {
    expect(formatPrice(99999999)).toBe('¥999999.99')
  })
})
text 复制代码
// 运行结果
PASS  utils/__tests__/formatPrice.test.ts
  ✓ handles null gracefully (2ms)
  ✓ handles undefined gracefully (1ms)
  ✓ handles negative numbers (1ms)
  ✓ handles large numbers without overflow (2ms)

Tests: 4 passed, 4 total

第二步:加类型守卫。 不改调用方,在函数入口做防御。

typescript 复制代码
export function formatPrice(cents: number | null | undefined): string {
  if (cents == null) {
    return '¥0.00'
  }
  return `¥${(cents / 100).toFixed(2)}`
}

第三步:沉淀检查清单。 以后AI写测试,人审的时候照着这个清单过一遍,比凭经验靠谱。本文测试环境基于 Jest 29.7 + TypeScript 5.5,strictNullChecks 已开启。

javascript 复制代码
AI写测试人审检查清单:
□ 有没有覆盖 null/undefined 输入?
□ 有没有覆盖空数组/空对象?
□ 有没有覆盖边界值(0、负数、极大/极小)?
□ 有没有覆盖 API 超时/报错的场景?
□ 断言是只写了"等于什么",还是也写了"不做什么"?

人 vs AI 写测试的思维差异

这次翻车让我重新想了一件事:人写测试和AI写测试,到底差在哪?

维度 人写测试 AI写测试
输入来源 调用方的实际使用场景 + 业务知识 函数签名 + JSDoc + 代码实现
测试思维 防御性("别人会怎么坑我") 乐观性("别人会按文档用")
边界覆盖 基于经验("这个参数线上传过null") 基于类型("类型是number,传number就行")
异常断言 会写"不应该走到这里"的断言 只写"走到这里应该返回什么"

人的测试思维是防御性的 ------写测试的时候会想"调用方可能怎么误用这个函数","线上实际跑的时候会产生什么垃圾数据"。AI的测试思维是乐观性的------基于"这个函数按设计应该怎么用"来生成测试,不会主动考虑异常情况。

这不是AI的缺陷,这是它的信息边界决定的。人知道"这个API之前返回过null",AI不知道。人知道"这个函数被三个地方调用,其中一个调用方传参不规范",AI不知道。AI写测试的质量,取决于你给了它多少上下文。

边界:AI写测试什么场景好用,什么场景不好用

经过这次翻车,我后来定了几个规则:

AI写测试好用的场景:

  • 纯函数/工具类,输入输出明确
  • 数据转换(JSON parse/stringify、格式转换)
  • 类型定义和校验函数
  • 脚手架代码的初始化测试

AI写测试不好用的场景:

  • 业务逻辑复杂、分支多的代码
  • 依赖外部状态(数据库、文件系统、第三方API)
  • 多步操作(需验证"某一步执行了,某一步没执行")
  • 涉及异步时序的代码

对于"不好用"的场景,让AI写测试骨架,人补断言和边界。对于"好用"的场景,也不是完全信任------人审的时候至少过一遍检查清单。

一个format函数教会我的事

回头来看,这个翻车的问题不在AI,在我。

我以为"让AI写测试"是"让AI帮我把测试写了"------节省时间,提高效率。但实际不是。"让AI写测试"的正确姿势是"让AI起草测试,人来做测试该做的事"------判断什么值得测,什么不值得测,什么场景下会出问题。

AI写测试最大的价值不是"替代你写测试",是"用最快的速度覆盖正常路径,把你解放出来去思考边界路径"。这个认知不对,翻车就是迟早的事。

相关推荐
码途AI工坊1 小时前
大模型时代必修课:懂 Token 的人,用 1 块钱跑出别人 100 块钱的效果。
ai编程
Jul1en_2 小时前
Matt 与 Uncle Bob 的播客访谈有感
开发语言·经验分享·笔记·ai·开源·github·ai编程
是2的10次方啊2 小时前
AI 能写代码了,还要学设计模式、Spring 源码和 JVM 吗?
ai编程
全栈弄潮儿10 小时前
不要先问“用哪个 AI”,先盘点你的开发工作流
aigc·openai·ai编程
杨杨杨大侠11 小时前
大模型的权重到底怎么用?拆开一个 token 的生成过程
aigc·openai·ai编程
吴佳浩 Alben12 小时前
Agent 怎么做自动化评测?构建端到端的 Agent Evaluation 体系
人工智能·语言模型·架构·自动化·ai编程
知了一笑12 小时前
圈外人焦虑AI吗?
人工智能·ai·aigc
小虎AI生活13 小时前
从四大模型一周连发看企业 AI 落地,为什么 95% 的试点不赚钱
ai编程
Dawson Zhu13 小时前
从单体到联邦:多Agent架构的必要性与设计哲学
人工智能·语言模型·架构·aigc·agi