前端单元测试入门,Jest从零配置到实战

"前端需要写单元测试吗?"------很多人觉得前端页面交互为主,测试意义不大。

但实际上,工具函数、状态管理、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%覆盖率,把核心工具函数和关键业务逻辑覆盖到就很有价值了。

建议从新写的代码开始加测试,老代码慢慢补。

相关推荐
AI工具人PM产品经理1 个月前
Claude Code TDD 实战:84 个测试护体的 spec→plan 开发流
jest·tdd·工作流·ai 编程·claude code
kyriewen5 个月前
你写的代码没有测试,就像出门不锁门——Jest + Testing Library 从入门到不慌
前端·单元测试·jest
时79 个月前
前端项目测试覆盖率检测
前端·jest
大喜xi1 年前
Jest 使用指南:配置、实际应用与常用 API
前端·jest
codefish7981 年前
深度解读前端测试:Jest vs Vitest 差异分析
前端·jest
夜寒花碎2 年前
前端自动化测试一jest基础使用
前端·单元测试·jest
摸爬滚打的小李2 年前
单元测试
jest
摸爬滚打的小李2 年前
单元测试--jest
jest
ihengshuai2 年前
Jest单元测试
单元测试·jest·前端自动化测试