前端的单元测试,大部分都是在自欺欺人

在前端圈,单元测试 几乎是一个非常头疼的话题。简历上不写熟悉单元测试,都不好意思跟人打招呼了。CI/CD流程里,要是没有一个test的阶段,就好像这个项目不够专业。而那绿色的"Coverage: 95%",也常常被当作项目质量的黄金标准,成为许多团队KPI的一部分。

但在我带团队的这几年,尤其是在经历了几个项目因为高覆盖率的单元测试而举步维艰之后,我越来越倾向于一个结论:

我们前端领域里,大部分团队写的大部分所谓单元测试,纯属自欺欺人。

在你说我反测试之前,请允许我加一个前提:我反对的,不是测试本身,而是那种为了覆盖率而写的、脱离用户实际场景的、脆弱不堪的、以测试实现细节为乐的单元测试


单元测试,长什么样?

我们先来看几个我从过往项目中挖出来的、非常典型的测试案例。

假设我们有一个简单的计数器组件:

JavaScript 复制代码
// Counter.jsx
class Counter extends React.Component {
  state = { count: 0 };
  increment = () => this.setState({ count: this.state.count + 1 });
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

然后,一份看起来正确的单元测试:

JavaScript 复制代码
// Counter.test.js
it('should increment count when button is clicked', () => {
  const wrapper = shallow(<Counter />);
  wrapper.find('button').simulate('click');
  // 重点:测试组件的 state
  expect(wrapper.state('count')).toBe(1); 
});

为什么说这是自欺欺人?

这个测试,和组件的内部实现(this.state.count)牢牢地绑在了一起。明天,我如果用Hooks重构这个组件,把state换成了useState,组件给用户看的功能完全没变,但这个测试百分之百会挂掉。

Mock测试

我们经常会写一些依赖其他模块或Hook的组件。于是,测试代码就变成了:

JavaScript 复制代码
// 一个依赖了 useAuth 和 useApi 的组件
jest.mock('./hooks/useAuth');
jest.mock('./api/user');

it('should display user name when logged in', () => {
  // 模拟 useAuth 返回已登录
  useAuth.mockReturnValue({ isLoggedIn: true, user: { name: 'John' } });
  // 模拟 api 调用
  userApi.getInfo.mockResolvedValue({ success: true, data: ... });
  
  render(<UserProfile />);
  // ...断言
});

当你的测试文件里,jest.mock的代码比断言的代码还多时,你就应该警惕了。我们到底是在测试我们的组件逻辑,还是在测试我们Mock的能力?这种测试,一旦组件的依赖关系发生微小的变化,整个测试文件可能都要重写。


这些写法带来了什么恶果?

这些测试累积起来,会给团队带来灾难性的后果。

团队看着CI上那95%的覆盖率,觉得高枕无忧。但实际上,这些测试只保证了"当A函数的输入是1时,返回值是2",却完全保证不了"当用户在页面上完成A、B、C三个操作后,页面能正确跳转"。高覆盖率,不等于高质量。

对重构的巨大阻力

这才是最致命的。

开发者不敢动那些老代码,不是因为怕改出Bug,而是因为怕改完之后,要花双倍的时间去修复那些因为实现细节变更而大面积失败的、脆弱的单元测试。这极大地扼杀了项目的迭代和优化动力。

浪费宝贵的开发时间

团队投入大量时间,去编写和维护这些弱鸡😒的测试,仅仅是为了让CI上的那个数字好看一点。这些时间,本可以用来做更有价值的事情,比如写更重要的集成测试,或者直接去优化产品功能。


我的方法论:像用户一样去测试

聊了这么多问题,那到底什么样的测试,才不是自欺欺人?

我的核心观点是:前端测试的重心,应该从单元测试,向集成测试和端到端测试倾斜。

也就是说:测试你的软件,而不是你的实现细节。

我们把第一个计数器的例子,用这种思想重新写一遍(使用React Testing Library):

JavaScript 复制代码
// Counter.test.js
import { render, screen, fireEvent } from '@testing-library/react';

it('should display the updated count after clicking the button', () => {
  render(<Counter />);
  
  // 找到屏幕上的按钮并点击它,模拟用户的真实操作
  const button = screen.getByRole('button', { name: /increment/i });
  fireEvent.click(button);
  
  // 验证用户在屏幕上看到的结果,而不是内部状态
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

这才是一个有价值的测试!

你看,这个测试,完全不关心 count这个状态是存在classstate里,还是useState里。它只关心一件事:"当用户点击了那个叫Increment的按钮后,他是不是能在屏幕上看到Count: 1这段文字? "

无论你未来怎么重构Counter组件,只要它的用户行为没有变,这个测试就不会失败。这才是我们需要的、健壮的、能给予我们信心的测试。


最后的最后

我们应该把大部分精力,投入到模拟用户真实操作路径的测试上,确保应用的功能 是正确的。而对于那些纯粹的、无副作用的工具函数(比如一个formatDate函数),单元测试依然是必要的,但这在我们的代码库里,只占很小一部分。

测试不是目的,保证软件能正常工作才是。

是时候审视一下我们项目里的*.test.js文件了,然后问问自己:

我写的这些测试,到底是在保证质量? 还是在自我安慰,看起来很牛皮?

你们怎么看?🤔

相关推荐
恋猫de小郭6 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅13 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606113 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了14 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅14 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅14 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment14 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅15 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊15 小时前
jwt介绍
前端