"前端需要写单元测试吗?"------很多人觉得前端页面交互为主,测试意义不大。
但实际上,工具函数、状态管理、API调用逻辑......这些不依赖DOM的部分,单元测试能帮你提前发现大量bug。
今天用Jest从零搭建前端测试环境。
为什么需要单元测试
| 没有测试 | 有测试 |
|---|---|
| 改一行代码怕影响全局 | 跑一遍测试就知道有没有影响 |
| 手动点点点回归 | 自动化秒级验证 |
| 新人不敢改代码 | 测试就是活文档 |
| bug上线后才发现 | 开发阶段就拦截 |
Jest基础配置
bash
# 安装
npm install jest @types/jest -D
# package.json加脚本
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
javascript
// jest.config.js
module.exports = {
testEnvironment: 'jsdom', // 模拟浏览器环境
transform: {
'^.+\\.jsx?$': 'babel-jest', // 支持ES6+
'^.+\\.vue$': '@vue/vue3-jest' // 支持Vue文件
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1' // 支持@路径别名
},
collectCoverageFrom: [
'src/**/*.{js,vue}',
'!src/main.js',
'!src/**/*.test.js'
]
};
测试工具函数
javascript
// src/utils/format.js
export function formatMoney(amount) {
return amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
export function formatDate(date, format = 'YYYY-MM-DD') {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day);
}
// src/utils/__tests__/format.test.js
import { formatMoney, formatDate } from '../format';
describe('formatMoney', () => {
test('格式化普通金额', () => {
expect(formatMoney(1234567)).toBe('1,234,567.00');
});
test('格式化小数', () => {
expect(formatMoney(99.9)).toBe('99.90');
});
test('格式化零', () => {
expect(formatMoney(0)).toBe('0.00');
});
test('格式化负数', () => {
expect(formatMoney(-1000)).toBe('-1,000.00');
});
});
describe('formatDate', () => {
test('默认格式', () => {
expect(formatDate('2026-10-13')).toBe('2026-10-13');
});
test('补零', () => {
expect(formatDate('2026-1-5')).toBe('2026-01-05');
});
});
常用断言
javascript
// 相等
expect(1 + 1).toBe(2);
expect({ name: '张三' }).toEqual({ name: '张三' });
// 真值
expect(true).toBeTruthy();
expect(0).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// 数字
expect(10).toBeGreaterThan(5);
expect(10).toBeLessThan(20);
expect(0.1 + 0.2).toBeCloseTo(0.3); // 浮点数比较
// 字符串
expect('hello world').toMatch(/hello/);
expect('hello').toContain('ell');
// 数组
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
// 异常
expect(() => { throw new Error('boom'); }).toThrow('boom');
// 异步
test('async test', async () => {
const data = await fetchData();
expect(data).toEqual({ name: '张三' });
});
Mock:隔离外部依赖
javascript
// src/api/user.js
import axios from 'axios';
export const getUser = (id) => axios.get(`/api/user/${id}`).then(r => r.data);
// src/services/userService.js
import { getUser } from '@/api/user';
export async function getUserInfo(id) {
const user = await getUser(id);
return { ...user, displayName: `${user.name}(${user.age})` };
}
// 测试中mock掉API
import { getUserInfo } from '../userService';
import * as userApi from '@/api/user';
jest.mock('@/api/user');
test('getUserInfo应该返回带displayName的用户信息', async () => {
// mock返回值
userApi.getUser.mockResolvedValue({ name: '张三', age: 25 });
const result = await getUserInfo(1);
expect(result.displayName).toBe('张三(25)');
expect(userApi.getUser).toHaveBeenCalledWith(1);
});
测试Vue组件
javascript
// src/components/Counter.vue
<template>
<div>
<span class="count">{{ count }}</span>
<button @click="increment">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => { count.value++; };
</script>
// 测试
import { mount } from '@vue/test-utils';
import Counter from '../Counter.vue';
test('点击按钮后count加1', async () => {
const wrapper = mount(Counter);
expect(wrapper.find('.count').text()).toBe('0');
await wrapper.find('button').trigger('click');
expect(wrapper.find('.count').text()).toBe('1');
});
test('初始props能设置初始值', () => {
const wrapper = mount(Counter, {
props: { initialCount: 10 }
});
// 如果有props的话
});
覆盖率
bash
npm run test:coverage
lua
----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
format.js| 100 | 100 | 100 | 100 |
validate.js| 85.7 | 75 | 100 | 85.7 |
----------|---------|----------|---------|---------|
| 指标 | 含义 |
|---|---|
| Statements | 语句覆盖率 |
| Branches | 分支覆盖率(if/else) |
| Functions | 函数覆盖率 |
| Lines | 行覆盖率 |
前端单元测试不需要追求100%覆盖率,把核心工具函数和关键业务逻辑覆盖到就很有价值了。
建议从新写的代码开始加测试,老代码慢慢补。