Jest 深入解析

FreeGuideOnline 最新 2026-07-14

bash npm install --save-dev jest


在 `package.json` 中添加一条脚本:

```json
"scripts": {
  "test": "jest"
}

创建第一个测试文件 sum.test.js

function sum(a, b) {
  return a + b;
}

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

执行 npm test,Jest 会自动找到并运行以 .test.js.spec.js 结尾的文件。这种约定优于配置的方式大幅降低了入门门槛。

全局测试方法

Jest 将常用的测试函数注入为全局变量,你无需在测试文件中逐个引入。

testit

两者完全等价,ittest 的别名,用于更接近行为描述风格的写法。

test('描述测试用例', () => {
  // 断言
});

it('应该返回正确的值', () => {
  // 断言
});

describe

用于对相关测试用例进行分组,可嵌套使用。

describe('算术运算', () => {
  describe('加法', () => {
    it('1 + 2 = 3', () => {
      expect(1 + 2).toBe(3);
    });
  });
});

生命周期钩子

在测试运行的前后执行设置与清理逻辑。

  • beforeAll / afterAll:块内所有测试执行前/后运行一次。
  • beforeEach / afterEach:块内每个测试执行前/后都运行。
describe('用户模块', () => {
  beforeAll(() => {
    // 连接数据库(一次)
  });

  beforeEach(() => {
    // 重置测试数据
  });

  afterEach(() => {
    // 清理模拟函数
  });
});

内置匹配器:语义化的断言

Jest 提供了丰富的匹配器(matchers),让断言意图一目了然。

相等性匹配

  • toBe:使用 Object.is 比较原始值(如数字、字符串、布尔值)。
  • toEqual:递归比较对象或数组的每个字段。
expect(2 + 2).toBe(4);
expect({ name: 'Jest' }).toEqual({ name: 'Jest' });

真值判断

  • toBeNulltoBeUndefinedtoBeDefinedtoBeTruthytoBeFalsy

数字比较

  • toBeGreaterThantoBeLessThantoBeCloseTo(浮点近似比较)。

字符串

  • toMatch:使用正则表达式匹配。
expect('Hello Jest').toMatch(/jest/i);

数组与可迭代对象

  • toContain:检查数组中是否包含某个元素。
expect(['apple', 'banana']).toContain('banana');

异常

  • toThrow:检查函数是否抛出错误,可指定错误信息或正则。
const errorFn = () => {
  throw new Error('something wrong');
};
expect(errorFn).toThrow('something wrong');

自定义匹配器可以通过 expect.extend 扩展,使断言更贴近业务。

异步代码测试

Jest 原生支持 Promise、async/await 以及回调风格的异步测试,只需让测试函数返回 Promise 或使用 done 回调。

Promise

test('fetch returns data', () => {
  return fetchData().then(data => {
    expect(data).toEqual({ id: 1 });
  });
});

Async / Await(推荐)

test('fetch returns data with async/await', async () => {
  const data = await fetchData();
  expect(data).toEqual({ id: 1 });
});

错误处理

test('fetch fails with error', async () => {
  await expect(fetchData()).rejects.toThrow('Network error');
});

回调与 done

如果被测代码使用回调而非 Promise,可通过调用 done 告诉 Jest 测试结束。

test('callback style', done => {
  fetchDataCallback((error, data) => {
    if (error) {
      done(error);
    } else {
      expect(data).toBeDefined();
      done();
    }
  });
});

注意:若忘记调用 done 或因错误未传递而超时,Jest 会在 5 秒后标记测试失败。

模拟函数与模块

模拟(Mock)是单元测试的基石,用于隔离被测代码与外部依赖。

jest.fn:创建模拟函数

const mockCallback = jest.fn(x => 42 + x);
[0, 1].forEach(mockCallback);

expect(mockCallback.mock.calls.length).toBe(2);          // 调用次数
expect(mockCallback.mock.calls[0][0]).toBe(0);           // 第一次调用的参数
expect(mockCallback.mock.results[0].value).toBe(42);     // 第一次返回值

常用模拟成员:

  • mockFn.mock.calls:所有调用的参数数组
  • mockFn.mock.results:所有调用的返回值/抛出对象
  • mockFn.mock.instancesnew 实例化时的 this 上下文

便捷添加:

  • mockReturnValue / mockReturnValueOnce
  • mockImplementation / mockImplementationOnce
  • mockResolvedValue / mockRejectedValue(用于 Promise)

jest.mock:自动模拟整个模块

假设存在一个模块 api.js

// api.js
export const fetchUser = () => axios.get('/user');

在测试中模拟它:

jest.mock('./api');
import { fetchUser } from './api';

// fetchUser 已被替换为自动生成的 mock
fetchUser.mockResolvedValue({ name: 'John' });

模块中的所有导出函数都会自动变为 jest.fn,你可以安全地控制其行为。

手动模拟(__mocks__ 目录)

在模块同级目录创建 __mocks__ 文件夹,放置同名模拟文件,即可全局使用。

.
├── api
│   ├── __mocks__
│   │   └── api.js
│   └── api.js

在测试文件中调用 jest.mock('./api') 时,Jest 会优先使用 __mocks__ 下的实现。这适合模拟复杂库或类。

部分模拟与 jest.requireActual

有时你只希望模拟模块的一部分,其余部分保持原样。

jest.mock('./utils', () => {
  const originalModule = jest.requireActual('./utils');
  return {
    ...originalModule,
    writeToDisk: jest.fn(),           // 模拟特定函数
  };
});

快照测试

快照测试用于捕获 UI 组件的渲染输出,防止意外变更。常与 React 等框架配合使用,但实际上可序列化的数据皆可快照。

test('renders correctly', () => {
  const tree = renderer.create(<MyComponent />).toJSON();
  expect(tree).toMatchSnapshot();
});

首次运行生成快照文件,存储在 __snapshots__ 目录。后续运行会对比当前输出与快照,不一致则测试失败。若变更是预期的,按 u 更新快照。

最佳实践

  • 快照应作为代码的一部分提交至版本库。
  • 保持快照小而专注,避免巨型组件快照。
  • 警惕自动更新快照,确保变更意图明确。

覆盖率报告

Jest 内置代码覆盖率工具(基于 Istanbul),无需额外安装配置。

jest --coverage

package.json 中配置:

"jest": {
  "collectCoverage": true,
  "collectCoverageFrom": [
    "src/**/*.{js,jsx}",
    "!src/**/*.test.{js,jsx}",
    "!src/index.js"
  ]
}

运行后生成 coverage 目录,包含 HTML 报告。终端也会显示语句、分支、函数、行的覆盖率百分比。

配置 Jest:按需定制

尽管标榜零配置,复杂项目仍需定制。可通过以下任一方式:

  • package.json 中的 jest 字段
  • 独立文件 jest.config.jsjest.config.ts.jestrc

常用配置项:

module.exports = {
  testEnvironment: 'node',          // 或 'jsdom',模拟浏览器环境
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
  transform: {
    '^.+\\.jsx?$': 'babel-jest',    // 如使用 Babel
  },
  moduleFileExtensions: ['js', 'jsx', 'json'],
  setupFilesAfterFramework: ['./jest.setup.js'], // 自定义设置
};

对于 React 项目,推荐使用 @testing-library/react 配合 jest,并在 setupFilesAfterFramework 中引入 @testing-library/jest-dom,以获得更丰富的 DOM 匹配器。

深度调试技巧

只运行特定测试

  • 使用 --testNamePattern 或多文件夹路径限定范围。
  • 临时使用 test.onlyit.only 运行单个测试。
  • describe.only 运行某一套件。

交互式监视模式

jest --watch

在监视模式下按 w 查看更多命令:筛选文件名、只运行失败测试、更新快照等。

调试配置

在 VS Code 中创建 launch.json,添加 Jest 配置:

{
  "type": "node",
  "request": "launch",
  "name": "Jest Current File",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": [
    "--runTestsByPath",
    "${relativeFile}",
    "--config",
    "jest.config.js"
  ],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}

即可在测试文件中设置断点,按 F5 调试。

高性能并行与隔离

Jest 通过 jest-worker 并行运行测试文件,默认使用 CPU 核心数减一的线程数。在 CI 环境中可通过 --maxWorkers 控制。每个测试文件在独立的沙箱环境中运行,全局变量互相隔离,因此可以安全地并行。

持续集成集成

将 Jest 纳入 CI 流只需在配置文件中添加命令:

# GitHub Actions 示例
- name: Run Tests
  run: npm test -- --coverage

Jest 支持 JUnit 格式报告器(需 jest-junit),可集成到各类 CI 平台展示测试结果。

与 TypeScript 无缝协作

安装 ts-jest 或使用 babel 预设。

npm install --save-dev ts-jest @types/jest

创建 jest.config.js

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};