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

在前端圈,单元测试 几乎是一个非常头疼的话题。简历上不写熟悉单元测试,都不好意思跟人打招呼了。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文件了,然后问问自己:

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

你们怎么看?🤔

相关推荐
Jonathan Star1 小时前
沉浸式雨天海岸:用A-Frame打造WebXR互动场景
前端·javascript
工业甲酰苯胺2 小时前
实现 json path 来评估函数式解析器的损耗
java·前端·json
老前端的功夫2 小时前
Web应用的永生之术:PWA落地与实践深度指南
java·开发语言·前端·javascript·css·node.js
LilySesy2 小时前
ABAP+WHERE字段长度不一致报错解决
java·前端·javascript·bug·sap·abap·alv
Wang's Blog3 小时前
前端FAQ: Vue 3 与 Vue 2 相⽐有哪些重要的改进?
前端·javascript·vue.js
再希4 小时前
React+Tailwind CSS+Shadcn UI
前端·react.js·ui
用户47949283569154 小时前
JavaScript 的 NaN !== NaN 之谜:从 CPU 指令到 IEEE 754 标准的完整解密
前端·javascript
群联云防护小杜4 小时前
国产化环境下 Web 应用如何满足等保 2.0?从 Nginx 配置到 AI 防护实战
运维·前端·nginx
醉方休5 小时前
Web3.js 全面解析
前端·javascript·electron
前端开发爱好者5 小时前
前端新玩具:Vike 发布!
前端·javascript