单元测试"假绿"实录:测试全过但线上崩了,这 3 个坑你踩过吗?

问题场景

团队推行单元测试,CI 门禁要求覆盖率 ≥80%。你兢兢业业写了上百条用例,全部绿了,自信上线。

结果线上反馈:某个功能完全不可用,点按钮报错。

回查代码:实现逻辑确实错了。但------测试也的确通过了。

测试通过了 ≠ 代码正确。 这种"假绿"比没有测试更可怕,因为它给了你虚假的安全感。


原因分析 & 解决方案

🕳️ 坑 1:异步代码用 sync 断言------测试跑完时,异步还没跑完

typescript 复制代码
// ❌ 典型反例
it('should update user name', () => {
  const user = { name: 'old' };
  
  fetchUserName(1).then(name => {
    user.name = name;  // 异步回调
  });

  expect(user.name).toBe('Alice'); // ✅ 测试通过?不,执行时还是 'old'
});

这段测试 100% 通过------不是因为逻辑对了,而是 断言执行时 Promise 还没 resolve

解决方案 :使用 waitFor / findBy 或显式 await:

typescript 复制代码
// ✅ 正确做法
it('should update user name', async () => {
  const user = { name: 'old' };
  
  const promise = fetchUserName(1).then(name => {
    user.name = name;
  });

  await promise; // 重点!
  expect(user.name).toBe('Alice');
});

要点 :凡涉及 Promise/setTimeout/requestAnimationFrame 的测试,记住一个原则------断言必须在异步操作完成后执行


🕳️ 坑 2:mock 得太"完美"------测的全是假数据,真实逻辑根本没走

typescript 复制代码
// ❌ 典型反例
vi.mock('./api', () => ({
  fetchData: () => Promise.resolve({ status: 'ok' }),
}));

it('should handle response', async () => {
  const result = await fetchData(); // 直接返回 mock 值
  expect(result.status).toBe('ok'); // ✅ 永远通过
});

问题在哪? 你把整个 fetchData 都 mock 了------真实的 HTTP 请求、错误处理、数据解析逻辑完全没有被执行。

这就好比把真正的发动机拆掉,换成了一台永动机模型来测试。永远能跑,但没什么用。

解决方案:只 mock 边界(如网络层),不 mock 业务核心逻辑:

typescript 复制代码
// ✅ 正确做法:只 mock 网络请求工具本身
import axios from 'axios';
vi.spyOn(axios, 'get').mockResolvedValue({ data: { status: 'ok' } });

it('should handle response', async () => {
  const result = await fetchData(); // 真实 fetchData 逻辑执行
  expect(result.status).toBe('ok'); // 验证的是你的业务处理,而非 mock 返回值
});

要点 :Mock 是双刃剑。原则是------mock 你控制不了的部分(网络、时间、随机数),测试你能控制的部分(业务逻辑、数据转换)


🕳️ 坑 3:边界情况忽略------只测了"黄金路径"

typescript 复制代码
// ❌ 典型反例
function formatPrice(price: number, currency: string) {
  return `${currency}${price.toFixed(2)}`;
}

it('should format price correctly', () => {
  expect(formatPrice(10.5, '¥')).toBe('¥10.50'); // ✅ 通过
});

测试通过,看起来完美。然而:

typescript 复制代码
formatPrice(undefined, '¥');  // ❌ TypeError: Cannot read property 'toFixed'
formatPrice(10.5, '');        // 输出: "10.50"(一眼看不出来)
formatPrice(0.1 + 0.2, '¥'); // 输出: "¥0.30"(浮点精度还能接受...)
formatPrice(NaN, '¥');        // 输出: "¥NaN"(用户一脸懵)

解决方案 :对每个函数,养成"三角测试"习惯------正常值 + 边界值 + 异常值

typescript 复制代码
// ✅ 正确做法:补上边界用例
it('should handle edge cases', () => {
  expect(formatPrice(0, '¥')).toBe('¥0.00');
  
  // 异常输入应当有错误处理
  expect(() => formatPrice(undefined as any, '¥')).toThrow();
  expect(() => formatPrice(NaN, '¥')).toThrow();
});

要点 :写测试时,多问自己一句------如果输入是空值/负值/零/undefined/null,代码会怎样?


要点总结

# 假绿原因 一眼识别 根治方法
1 异步断言过早 测试里没有 async/await 但有异步操作 所有异步路径必须 await
2 Mock 过度 mock 了业务模块本身 只 mock 不可控的边界层
3 黄金路径偏执 测试数据全是"刚好能过的" 补上边界值 + 异常值用例

一个简单的自检清单:你的测试真正测试了什么?如果实现代码改成错的,测试会变红吗? 如果不能,那就是假绿------删掉它都比留着强。

相关推荐
oscar99910 小时前
4.6 单元测试的重构:把交易策略封装成可独立验证的函数
重构·单元测试·财富源代码
ck-joker21 小时前
23个测试全绿,一个Token没花——LLM应用的单元测试该怎么写?
语言模型·单元测试
Lucky_5566881 天前
软件登记测试报告用途与完整办理流程|山东本地第三方测评机构服务
经验分享·功能测试·单元测试
醉熏的斑马2 天前
ASP.Net MVC3 】使用Moq让单元测试变得更简单
单元测试·log4j·asp.net
ckjoker4 天前
23个测试全绿,一个Token没花——LLM应用的单元测试该怎么写?
单元测试·agent
川石课堂软件测试5 天前
性能测试|Nginx中间件监控与调优
linux·python·nginx·中间件·单元测试·压力测试·harmonyos
想你依然心痛7 天前
嵌入式单元测试:Unity/CMock框架与硬件在环测试——测试驱动、桩函数
unity·单元测试·游戏引擎
fzm52988 天前
车载ECU单元测试技术与应用研究
c语言·自动化测试·单元测试·嵌入式·白盒测试
许彰午25 天前
39_Java单元测试JUnit入门
java·junit·单元测试