Jest 前端测试:单元、快照与模拟

FreeGuideOnline 最新 2026-06-15

Jest 前端测试完全指南:单元、快照与模拟

在现代前端开发中,自动化测试是保障代码质量和可维护性的核心实践。Jest 作为 Facebook 开源的 JavaScript 测试框架,凭借零配置、快照测试和强大的模拟(Mock)生态,已成为前端测试的事实标准。本文将从零开始,带你系统掌握 Jest 的核心能力:编写可靠的单元测试、使用快照保护 UI 结构、以及灵活模拟依赖,让你的代码更加稳固。

为什么选择 Jest?

Jest 是为 JavaScript 应用设计的“全能型”测试框架,它不仅运行速度快,还内置了断言库、覆盖率报告和模拟功能,无需额外引入 Mocha、Chai 或 Sinon 等工具。对 React 项目而言,它更是官方推荐的测试方案(create-react-app 直接内置)。无论你使用 Vue、Angular 还是纯 TypeScript,Jest 都能无缝对接。

快速上手:安装与配置

首先,在你的前端项目中初始化 npm 并安装 Jest:

npm init -y
npm install --save-dev jest

package.json 中添加测试脚本:

{
  "scripts": {
    "test": "jest"
  }
}

现在你可以创建一个简单的函数文件 sum.js 和对应的测试文件 sum.test.js

sum.js

function sum(a, b) {
  return a + b;
}
module.exports = sum;

sum.test.js

const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

运行 npm test,Jest 会自动找到以 .test.js 结尾的文件并执行。如果项目使用 ES Modules 或 TypeScript,需要添加相应的 Babel 或 ts-jest 配置,但基本思想相同。

核心概念一:单元测试

单元测试的目标是验证应用中最小可测试单元(通常是函数或组件方法)的行为是否符合预期。Jest 提供了简洁的匹配器(matchers)来表达断言。

基本匹配器

  • toBe(value):严格相等(使用 Object.is
  • toEqual(value):深度比较对象或数组的值
  • toBeNull() / toBeDefined() / toBeUndefined()
  • toBeTruthy() / toBeFalsy()
  • toContain(item):检查数组包含某元素

示例:测试一个过滤函数

filterByTerm.js

function filterByTerm(inputArr, searchTerm) {
  if (!searchTerm) throw Error('searchTerm cannot be empty');
  const regex = new RegExp(searchTerm, 'i');
  return inputArr.filter(item => item.url.match(regex));
}
module.exports = filterByTerm;

filterByTerm.test.js

const filterByTerm = require('./filterByTerm');

describe('filterByTerm', () => {
  const input = [
    { id: 1, url: 'https://www.example.com' },
    { id: 2, url: 'https://www.test.com' }
  ];

  test('should filter by search term (example)', () => {
    const result = filterByTerm(input, 'example');
    expect(result).toEqual([{ id: 1, url: 'https://www.example.com' }]);
  });

  test('should be case insensitive', () => {
    const result = filterByTerm(input, 'EXAMPLE');
    expect(result).toHaveLength(1);
  });

  test('should throw when searchTerm is empty', () => {
    expect(() => filterByTerm(input, '')).toThrow('searchTerm cannot be empty');
  });
});

使用 describe 对相关测试进行分组,可以提升报告可读性。

测试异步代码

前端大量操作涉及 Promise 或回调,Jest 能轻松处理。

  • Promises:返回一个 Promise,Jest 会等它 resolve 或 reject。
test('fetches data successfully', () => {
  return fetchData().then(data => {
    expect(data.name).toBe('Jest');
  });
});
  • Async/Await:让代码更像同步。
test('fetches data with async/await', async () => {
  const data = await fetchData();
  expect(data.name).toBe('Jest');
});
  • 错误测试:使用 expect.assertions 验证一定数量的断言被调用,结合 rejects
test('fetches fails with error', async () => {
  expect.assertions(1);
  await expect(fetchData()).rejects.toThrow('Network error');
});

核心概念二:快照测试

快照测试是 Jest 的标志性功能,特别适合检测 UI 组件或数据结构是否意外改变。当测试首次运行时,Jest 会生成一个快照文件记录组件的渲染输出;后续测试会与快照比较,若有差异则提示开发者手动确认更新。

快照的使用场景

  • React/Vue 组件的渲染结果
  • 对象或数组的序列化结果
  • 任何需要保证输出结构不变的情况

编写 React 组件的快照测试

假设你有一个简单的 Button 组件:

// Button.jsx
import React from 'react';

const Button = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

export default Button;

对应的测试文件:

// Button.test.jsx
import React from 'react';
import renderer from 'react-test-renderer';
import Button from './Button';

test('Button renders correctly', () => {
  const tree = renderer
    .create(<Button label="Click me" onClick={() => {}} />)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

首次运行 npm test 后,项目根目录会生成 __snapshots__/Button.test.jsx.snap 文件:

// Jest Snapshot v1, ...

exports[`Button renders correctly 1`] = `
<button
  onClick={[Function]}>
  Click me
</button>
`;

此后,如果组件结构发生变化(例如标签改为 a),测试将失败并提示快照不匹配。你可以通过以下方式处理:

  • 如果变更是预期的,按 u 更新快照。
  • 如果是 bug,修复组件代码。

注意快照测试的“陷阱”

  • 提交快照文件到版本控制:团队成员共享同一份基准。
  • 不要过度依赖:快照只验证结构,不验证行为逻辑;因此需结合行为断言。
  • 保持快照小而专注:避免渲染大型页面,使用浅渲染(Shallow Render)或分解测试。

核心概念三:模拟(Mock)

在实际项目中,函数和模块往往依赖外部资源(API 请求、数据库、浏览器 API 等)。为了隔离被测试单元,我们需要用“模拟”来替代真实实现。Jest 提供了多种模拟方式,让你轻松控制依赖的行为。

函数模拟:jest.fn()

jest.fn() 可以创建一个模拟函数,用于检查调用次数、参数等。

test('mock function is called', () => {
  const mockCallback = jest.fn();
  [1, 2].forEach(mockCallback);
  expect(mockCallback).toHaveBeenCalledTimes(2);
  expect(mockCallback).toHaveBeenCalledWith(1);
  expect(mockCallback).toHaveBeenCalledWith(2);
});

你还可以指定模拟函数的返回值或实现:

const mock = jest.fn()
  .mockReturnValue(42)
  .mockReturnValueOnce('first call')
  .mockReturnValueOnce('second call');

console.log(mock(), mock(), mock(), mock());
// 'first call', 'second call', 42, 42

模块模拟:jest.mock()

当需要模拟整个模块(例如 axios)时,使用 jest.mock()

示例:模拟 axios 请求

假设有一个 fetchUser.js 使用 axios:

// fetchUser.js
import axios from 'axios';

export const fetchUser = async (id) => {
  const response = await axios.get(`/users/${id}`);
  return response.data;
};

测试文件模拟 axios,避免实际网络请求:

// fetchUser.test.js
import axios from 'axios';
import { fetchUser } from './fetchUser';

jest.mock('axios');

test('fetchUser returns user data', async () => {
  const user = { id: 1, name: 'Alice' };
  axios.get.mockResolvedValue({ data: user });

  const result = await fetchUser(1);
  expect(result).toEqual(user);
  expect(axios.get).toHaveBeenCalledWith('/users/1');
});

手动模拟与隐式模拟

Jest 支持在 __mocks__ 目录下放置手工编写的模拟文件。当你调用 jest.mock('./module') 时,Jest 会自动使用该目录中的模拟实现。这对于模拟 Node 核心模块(如 fs)或复杂第三方库非常有用。

手动模拟浏览器的 API

前端测试常需要模拟 window 对象或 DOM API。Jest 默认在 jsdom 环境下运行,但某些 API(如 localStorage)仍需手动模拟。

const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  clear: jest.fn()
};
Object.defineProperty(window, 'localStorage', { value: localStorageMock });

test('localStorage.setItem is called', () => {
  localStorage.setItem('key', 'value');
  expect(localStorage.setItem).toHaveBeenCalledWith('key', 'value');
});

进阶实践与最佳建议

测试组织:从简单到复杂

  1. 工具函数:纯函数逻辑最容易测试,应该覆盖所有主要路径和边界条件。
  2. React 组件:结合 @testing-library/react 进行行为测试,配合快照验证渲染结构。
  3. Redux/Vuex:单独测试 reducer、action、mutation 等纯函数部分;异步 action 可以模拟 dispatch。
  4. API 服务层:模拟 HTTP 客户端,验证请求参数和数据处理。

持续集成与覆盖率

package.json 中添加覆盖率脚本:

{
  "scripts": {
    "test:coverage": "jest --coverage"
  }
}

Jest 会生成 coverage 目录,包含精美的 HTML 报告。设定覆盖率阈值(在 jest.config.jspackage.json 中)可以防止提交未测试的代码。

配置 jest.config.js

虽然 Jest 支持零配置,但创建配置文件能更好地管理项目:

module.exports = {
  testEnvironment: 'jsdom',          // 默认 node,前端项目设为 jsdom
  moduleNameMapper: {               // 处理静态资源或别名
    '\\.(css|less)$': 'identity-obj-proxy'
  },
  setupFilesAfterSetup: [           // 在每个测试文件执行前的设置
    '<rootDir>/src/setupTests.js'
  ],
  collectCoverageFrom: ['src/**/*.{js,jsx}'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

避免常见误区

  • 过度模拟:不要模拟所有依赖,只模拟会引入副作用或减慢测试的模块(网络、定时器等)。
  • 测试实现细节:测试应关注行为,而非内部实现,否则重构会频繁破坏测试。
  • 忽略错误用例:覆盖错误路径和异常情况,比仅测快乐路径更有价值。
  • 快照过大:将庞大组件的快照拆分为小块,便于审查变更。

总结

Jest 为前端开发者提供了一套优雅且效率极高的测试工具体系。通过单元测试,你能确保逻辑函数的正确性;通过快照,你能快速捕获 UI 结构的变化;通过模拟,你能在隔离环境中测试复杂交互。当这些技术融入你的开发流程,代码的可靠性与重构信心将显著提升。现在就打开终端,为你的下一个功能编写第一个测试吧!