单元测试"假绿"实录:测试全过但线上崩了,这 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 黄金路径偏执 测试数据全是"刚好能过的" 补上边界值 + 异常值用例

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

相关推荐
梦想不只是梦与想1 天前
鸿蒙 AI自动化测试实现
单元测试·harmonyos·hypium
光影6271 天前
Unittest单元测试基础速记
测试工具·单元测试·json·测试覆盖率
龙智DevSecOps解决方案3 天前
TESSY基于需求的单元测试教程:需求关联、覆盖率与可疑测试处理
单元测试·视频教程·tessy
努力努力再努力wz3 天前
【Lua 入门系列】从顺序执行到分支与循环:一文理解 Lua 控制结构、作用域与跳转规则
junit·单元测试·lua
xiaoye-duck4 天前
《C++ 工具库实战》GTest 单元测试:断言、全局环境、局部夹具与底层原理
c++·单元测试·第三方库
小凯在掘金5 天前
测试规范:为什么优先用 data-testid,而不是 CSS 选择器?
单元测试·测试
2501_915106325 天前
iOS 单元测试怎么跑?用 KXApp 在 VS Code 里跑 XCTest 和 Swift Testing
ide·vscode·ios·单元测试·个人开发·swift·敏捷流程
小莫4618 天前
把「测试有效性」加进 CI:GitHub Actions 配置指南
单元测试
全栈技术负责人8 天前
AI 自动化单元测试技术方案
运维·单元测试·自动化
川石课堂软件测试11 天前
涨薪技术|0到1学会性能测试第9课-HTTP响应统计
数据库·功能测试·网络协议·http·kubernetes·单元测试·测试用例