刚开始我也以为前端测试就是写几个expect,结果连踩三个坑:测试跑得太慢、频繁假失败、维护成本比业务代码还高。后来花了几个月摸索,才找到一套相对靠谱的实践。今天就把这些血泪经验分享出来,希望能让你少走弯路。
开篇先放一张图:一个简单的登录页面,红色箭头指向测试覆盖的关键点(如输入验证、API 调用、错误提示)。这是测试的核心——不是覆盖所有代码,而是覆盖用户可能遇到的关键路径。
先别急着写代码,想清楚测什么
测试不是越多越好。我见过有人对每个工具函数都写三个测试,结果业务逻辑改了,测试崩溃一堆。更实际的做法是:优先测试用户交互和业务逻辑。
举个栗子,一个计数器组件:
“javascript
// Counter.jsx
import { useState } from 'react';
export default function Counter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return (
{count}
);
}
`
为什么要这么写:data-testid是测试的命脉,避免用 CSS 类名或文本内容定位,否则样式一变测试就崩。
对应的测试应该这样写:
`javascript
// Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('初始计数为0,点击+后变为1', () => {
render(
const countDisplay = screen.getByTestId('count');
expect(countDisplay).toHaveTextContent('0');
fireEvent.click(screen.getByText('+'));
expect(countDisplay).toHaveTextContent('1');
})
<
p>;
test('点击-不会让计数低于0', () => {
render(
const countDisplay = screen.getByTestId('count');
fireEvent.click(screen.getByText('-'));
expect(countDisplay).toHaveTextContent('0'); // 假设不允许负数
});
`
个人吐槽:很多人喜欢测“渲染后有没有按钮”,这种测试基本是废话。应该测的是“用户点了按钮后发生了什么”,这才是价值。
另一个坑:异步测试让人头疼
前端测试最烦的就是异步逻辑,尤其是 API 调用。官方文档文档不够清晰,很多人直接弃坑。其实有套路。
核心原则:模拟外部依赖,不要发真实请求。
用 msw(Mock Service Worker)拦截请求,比 jest.fn() 更贴近真实场景:
`javascript
// 假设有一个获取用户列表的组件
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
const server = setupServer(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json([{ id: 1, n
ame: 'Alice' }, { id: 2, name: 'Bob' }]));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('加载用户列表并显示', async () => {
render(
// 等待异步完成
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
});
`
为什么这么写:msw 不干扰真实网络栈,而且在测试退出后自动清理,不会造成内存泄漏。waitFor 是异步测试的关键,它会轮询直到条件满足或超时。
数据说话:用了 msw 后,我的测试从平均 3.2 秒降到了 0.8 秒,而且再也没出现过“请求超时”的假失败。
中间插入一张图:一个测试运行时间的柱状图,左边是优化前(3.2s),右边是优化后(0.8s)。标题:Mock 请求让测试快了 4 倍。
还有个技巧:测试覆盖率不是目标
很多团队把测试覆盖率设成 80% 甚至 100%,这完全是个误区。覆盖率低不代表质量差,覆盖率高也不代表没 Bug。
我现在的策略是:
- 核心业务逻辑(如支付流程):100% 覆盖
- 工具函数:80% 覆盖
- 纯 UI 组件:只测用户交互,不测内部状态
比如一个弹出框组件,我不关心它怎么渲染,只关心“用户点击关闭按钮后消失”。
真实案例:一个 Bug 让我崩溃 3 小时
去年有个项目,用户反馈“提交表单后偶尔没反应”。我查了日志没报错,怀疑是测试没覆盖到。后来发现是 useEffect 的依赖数组写错了:
`javascript
// 错误代码
useEffect(() => {
submitForm(data);
}, [data.name]); // 依赖只包含 name,忽略了其他字段
// 正确的测试应该写
test('表单提交使用最新数据', async () => {
const mockSubmit = jest.fn();
render(
// 先改 name
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Alice' } });
// 再改 email
fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'alice@test.com' } });
fireEvent.click(screen.getByText('Submit'));
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({ name: 'Alice', email: 'alice@test.com' });
});
});
`
这个测试会失败,因为 useEffect 没捕捉到 email 的变化。这就是测试的价值——不是测代码,是测行为。
E2E 测试:真实场景的护城河
单元测试和集成测试能覆盖大部分逻辑,但最终用户用的是浏览器。E2E 测试用 Cypress 或 Playwright 模拟真实操作。
我的建议:只写 3-5 个关键流程的 E2E 测试。 比如“用户注册-登录-查看订单”,多了维护不起。
Cypress 的一个小技巧:用 cy.intercept 拦截 API,确保测试稳定:
`javascript`
cy.intercept('POST', '/api/login', { token: 'fake-token' }).as('login');
cy.get('#username').type('testuser');
cy.get('#password').type('password123');
cy.get('button[type="submit"]').click();
cy.wait('@login');
cy.url().should('include', '/dashboard');
为什么这么写:不依赖真实后端,测试速度从 15 秒降到 5 秒,而且不会因为后端挂掉而全红。
总结一下:你可以立刻用的三个点
或 cy.intercept`,让测试跑得飞快。最后送一句话:测试不是负担,是你代码的保险。踩坑两次后,你会感谢自己的。
结尾再放一张图:一个简洁的时间线,展示“无测试”到“有测试”的 Bug 率下降趋势。标题:测试让 Bug 率降低 60%。