前端测试:别为了100%覆盖率而写测试,那是自欺欺人
你写了测试,覆盖率100%,感觉稳了。结果上线后,用户点了个按钮,页面直接白屏。你纳闷:覆盖率不是100%吗?因为你测的都是“天气好不好”,没测“会不会地震”。今天我们就来聊聊前端测试的正确姿势——怎么测才能真的有用,而不是为了指标好看写一堆废话。
前言
前端测试常走两个极端:要么完全不测,上线随缘;要么为了覆盖率,测了等于没测(比如测个1 + 1 = 2)。真正有效的测试,不是越多越好,而是该测的测,不该测的别浪费生命。
今天我们用“测试金字塔”模型,帮你理清单元测试、组件测试、E2E测试的分工。看完你会知道:哪部分代码必须测,哪部分可以跳过,哪部分用哪个工具。
一、测试金字塔:三分天下,各司其职
/\
/E2E\ ← 少而精,关键路径
/------\
/集成测试\ ← 中等,组件间交互
/----------\
/ 单元测试 \ ← 多而快,纯逻辑
/--------------\
- 底座(单元测试):测最小的代码单元(函数、工具类)。多、快、便宜。
- 中层(组件测试/集成测试):测几个单元结合后的行为(比如一个表单组件提交数据)。
- 顶层(E2E测试):模拟真实用户,测整个流程(从打开页面到点击到结果)。
比例大概是:单元测试占70%,集成测试20%,E2E 10%。不是死规定,但原则:底层测试成本低,多写;顶层测试维护成本高,只写关键路径。
二、单元测试:测逻辑,不测实现细节
单元测试的目标:给定输入,输出是否正确。不关心函数内部怎么实现的,只关心结果。
适合测的:
- 纯函数(输入输出确定,无副作用)。
- 业务规则(比如
calculateDiscount(price, level))。 - 工具函数(
formatDate、parseQuery)。
不适合测的(测了也白测):
- 框架内部逻辑(React的setState、Vue的响应式——那是框架的事)。
- 简单的getter/setter。
- 常量定义。
工具:Jest + Vitest(Vite项目推荐Vitest)。
例子:
// 要测的函数
function formatPrice(price, currency = '¥') {
return `${currency}${price.toFixed(2)}`;
}
// 测试
test('格式化价格', () => {
expect(formatPrice(10.5)).toBe('¥10.50');
expect(formatPrice(10.5, '$')).toBe('$10.50');
});
黄金法则:如果重构代码不破坏测试,说明你测的是行为,不是实现。
三、组件测试:测交互,不测样式
组件测试(React Testing Library / Vue Test Utils)的目标:模拟用户行为,检查组件渲染和交互是否正确。不关心DOM结构细节,只关心用户能看到什么、能做什么。
适合测的:
- 根据props渲染正确的内容。
- 点击按钮触发正确回调。
- 表单输入后数据变化。
- 异步加载显示loading状态。
不适合测的:
- CSS样式(那是视觉回归测试的事,交给视觉测试工具)。
- 内部state的具体值(优先测渲染结果)。
- 第三方UI库的行为(假设它没问题)。
工具:React Testing Library + Jest(官方推荐),Vue Test Utils + Vitest。
例子(React Testing Library):
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('点击按钮增加计数', () => {
render(<Counter />);
const button = screen.getByText('增加');
fireEvent.click(button);
expect(screen.getByText('计数: 1')).toBeInTheDocument();
});
原则:测用户能看到的东西,不要测内部实现。
四、E2E测试:测关键用户旅程,不测所有交互
E2E测试模拟真实浏览器,跑完整的用户流程。它最像真实用户,但也最慢、最脆弱(网络波动、页面改动容易挂)。
适合测的(3-5个核心流程):
- 登录 → 访问个人主页 → 修改头像。
- 搜索商品 → 加入购物车 → 结算 → 支付成功。
- 未登录访问受保护页面 → 跳转到登录页。
不适合测的:
- 每个细节(比如每个按钮的悬浮效果)。
- 容易变化的面包屑导航。
- 第三方依赖的页面。
工具:Cypress(最友好)、Playwright(更可靠)、Puppeteer(较底层)。
例子(Cypress):
describe('用户登录', () => {
it('输入正确账号密码后跳转到首页', () => {
cy.visit('/login');
cy.get('[data-cy=username]').type('user@example.com');
cy.get('[data-cy=password]').type('password123');
cy.get('[data-cy=submit]').click();
cy.url().should('include', '/dashboard');
cy.contains('欢迎回来', { timeout: 10000 });
});
});
维护技巧:给关键元素加上data-cy属性,避免改样式或文本时测试挂掉。
五、测试覆盖率的谎言
很多团队追求100%覆盖率,结果工程师花大量时间测无关紧要的代码(比如测Redux的action creator是个纯对象)。覆盖率工具(Istanbul)只能告诉你“哪些代码没执行过”,不能告诉你“没测到的重要逻辑”。有时100%覆盖率,却漏掉了一个关键的空值判断。
正确的覆盖率指标:
- 核心业务逻辑达到80%以上就行。
- UI组件覆盖率参考即可,不必强求。
- 关注未覆盖的重要代码,而不是数字。
六、组合策略:一个电商网站的例子
- 单元测试:计算折扣、格式化价格、校验表单规则。Jest跑得快,每次提交都跑。
- 组件测试:商品卡片(渲染正确信息)、购物车弹窗(增加/删除商品)、地址表单(提交按钮禁用直到填写完整)。
- E2E测试:1. 用户搜索“手机” → 2. 添加第一个商品到购物车 → 3. 登录 → 4. 结算 → 5. 确认订单。就这一个核心流程,保证不崩。
日常开发:单元测试 + 组件测试在CI里跑(每次push)。E2E测试单独流水线,部署前跑一次(因为慢)。
七、测试不是银弹,别为了写而写
- 重构旧代码,没测试?先别补。补一个挂一个,浪费时间。优先补新功能。
- 一个bug反复出现,才需要补测试。
- UI改得频繁的区域,不写E2E,写组件测试更稳。
测试是手段,不是目的。目的是信心:当你改完代码,测试全绿,你能放心上线。
八、总结:测试就像买保险
- 单元测试:车险,便宜,必须买。
- 组件测试:医疗险,中等,按需买。
- E2E测试:地震险,贵,只买最关键的。
别买一大堆没用的险,也别裸奔。