Jest 实战指南

FreeGuideOnline 最新 2026-07-15

bash npm install --save-dev jest


或使用 yarn:

```bash
yarn add --dev jest

编写待测模块

创建 src/sum.js

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

编写测试文件

Jest 默认查找项目中的 *.test.js*.spec.js 文件,或者放在 __tests__ 目录下的文件。创建 src/sum.test.js

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

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

运行测试

package.jsonscripts 中添加:

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

然后运行:

npm test

你将看到类似下面的输出:

PASS  src/sum.test.js
  ✓ adds 1 + 2 to equal 3 (2 ms)

匹配器:断言的核心

基础匹配器

  • toBe(value):严格相等(相当于 ===
  • toEqual(value):深度相等,适用于对象和数组
  • not:取反
test('object assignment', () => {
  const data = { one: 1 };
  data['two'] = 2;
  expect(data).toEqual({ one: 1, two: 2 });
  expect(data).not.toBe({ one: 1, two: 2 }); // 不同引用
});

真假值判断

  • toBeNull()
  • toBeUndefined()
  • toBeDefined()
  • toBeTruthy()
  • toBeFalsy()
test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

数字匹配

  • toBeGreaterThan(number)
  • toBeGreaterThanOrEqual(number)
  • toBeLessThan(number)
  • toBeLessThanOrEqual(number)
  • toBeCloseTo(number, numDigits?):处理浮点数精度
test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // 浮点数比较
  expect(0.1 + 0.2).toBeCloseTo(0.3);
});

字符串匹配

  • toMatch(regexp | string):包含子串或匹配正则
test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

数组和可迭代对象

  • toContain(item):检查元素是否在数组中
test('the shopping list has milk on it', () => {
  const shoppingList = ['diapers', 'kleenex', 'trash bags', 'milk'];
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});

异常匹配

  • toThrow(error?):函数调用抛出异常
function compileAndroidCode() {
  throw new Error('you are using the wrong JDK');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);
});

异步代码测试

回调函数

使用 done 回调通知 Jest 测试完成。

test('the data is peanut butter', (done) => {
  function callback(error, data) {
    if (error) {
      done(error);
      return;
    }
    try {
      expect(data).toBe('peanut butter');
      done();
    } catch (error) {
      done(error);
    }
  }
  fetchData(callback); // 假设异步函数
});

Promise

可以直接返回 Promise,Jest 会等待其 resolve/reject。

test('the data is peanut butter', () => {
  return fetchData().then((data) => {
    expect(data).toBe('peanut butter');
  });
});

test('the fetch fails with an error', () => {
  expect.assertions(1); // 确保至少有一个断言被调用
  return fetchData().catch((e) =>
    expect(e).toMatch('error')
  );
});

使用 .resolves / .rejects 匹配器(更简洁):

test('the data is peanut butter', () => {
  return expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', () => {
  return expect(fetchData()).rejects.toMatch('error');
});

async / await

test('the data is peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});

可以结合 resolves / rejects

test('the data is peanut butter', async () => {
  await expect(fetchData()).resolves.toBe('peanut butter');
});

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

Mock 函数与模块

使用 jest.fn() 创建模拟函数

test('mock function basic', () => {
  const mockFn = jest.fn();
  mockFn('hello', 42);
  
  // 检查调用信息
  expect(mockFn).toHaveBeenCalled();
  expect(mockFn).toHaveBeenCalledTimes(1);
  expect(mockFn).toHaveBeenCalledWith('hello', 42);
});

可以自定义模拟行为:

test('mock return value', () => {
  const mockFn = jest.fn().mockReturnValue(10);
  expect(mockFn()).toBe(10);
});

test('mock implementation', () => {
  const mockFn = jest.fn((a, b) => a + b);
  expect(mockFn(2, 3)).toBe(5);
});

模拟模块

假设有 src/api.js

const axios = require('axios');
const fetchUser = (id) => axios.get(`/users/${id}`).then(res => res.data);
module.exports = fetchUser;

不发送真实网络请求,使用 jest.mock() 整体模拟:

jest.mock('axios');
const axios = require('axios');
const fetchUser = require('./api');

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

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

部分模拟与监视

使用 jest.spyOn() 保留真实实现,但可以监听调用:

const math = {
  add: (a, b) => a + b,
};

test('spyOn keeps original implementation', () => {
  const addSpy = jest.spyOn(math, 'add');
  const result = math.add(2, 3);
  
  expect(addSpy).toHaveBeenCalledWith(2, 3);
  expect(result).toBe(5);
  
  addSpy.mockRestore();
});

也可以用 jest.spyOn 模拟返回值:

jest.spyOn(math, 'add').mockReturnValue(99);
expect(math.add(1, 2)).toBe(99);

快照测试

快照测试是 Jest 的一大亮点,特别适合测试 UI 组件不会意外改变。

配置 react-test-renderer(示例)

npm install --save-dev react-test-renderer

编写快照测试

import renderer from 'react-test-renderer';
import Link from '../Link';

it('renders correctly', () => {
  const tree = renderer
    .create(<Link page="http://www.facebook.com">Facebook</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

首次执行会生成 __snapshots__ 目录和快照文件。之后每次运行都会与快照比较,不匹配时测试失败。若更新是有意的,添加 -u 参数更新快照:

jest --updateSnapshot

行内快照

可以将快照值内嵌在测试代码中,通过 toMatchInlineSnapshot() 实现,适合小型数据。

test('generates correct greeting', () => {
  expect(generateGreeting('John')).toMatchInlineSnapshot(`"Hello, John!"`);
});

配置 Jest

常用配置项

Jest 支持 jest.config.jsjest.config.tsjest.config.json 或在 package.json 中的 jest 字段。常用配置:

module.exports = {
  testEnvironment: 'node',          // 或 'jsdom'
  roots: ['<rootDir>/src'],         // 测试文件根目录
  testMatch: [                      // 匹配测试文件模式
    '**/__tests__/**/*.js?(x)',
    '**/?(*.)+(spec|test).js?(x)'
  ],
  moduleFileExtensions: ['js', 'jsx', 'json', 'node'],
  collectCoverageFrom: ['src/**/*.{js,jsx}'], // 覆盖率收集范围
  coverageThreshold: {               // 覆盖率阈值
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  setupFilesAfterSetup: ['./jest.setup.js'], // 测试全局启动文件
  transform: {                       // 转换器配置
    '^.+\\.jsx?$': 'babel-jest',
  },
};

使用 Babel 转换 ES Module

安装依赖:

npm install --save-dev babel-jest @babel/core @babel/preset-env

在项目根目录创建 babel.config.js

module.exports = {
  presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
};

现在就可以在测试中使用 import/export 语法。

环境设置:jest-environment-jsdom 用于 DOM 相关测试

安装:

npm install --save-dev jest-environment-jsdom

在文件头部添加注释启用:

/**
 * @jest-environment jsdom
 */

或在全局配置中设置 testEnvironment: 'jsdom'

测试覆盖率

运行测试时添加 --coverage 参数:

npm test -- --coverage

Jest 会生成 coverage 目录,包含 HTML 报告。在浏览器中打开 coverage/lcov-report/index.html 可查看详细的代码覆盖情况。

通过配置 collectCoverageFromcoverageThreshold 可以精细控制范围和最低标准,结合 CI/CD 保证代码质量。

实战技巧与最佳实践

组织测试结构

使用 describeit 替代 test,让测试更清晰:

describe('Array', () => {
  describe('#indexOf()', () => {
    it('should return -1 when the value is not present', () => {
      expect([1,2,3].indexOf(4)).toBe(-1);
    });
  });
});

使用 beforeEach / afterEach 管理重复准备逻辑

beforeEach(() => {
  initializeCityDatabase();
});

afterEach(() => {
  clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

仅运行特定测试

  • test.onlyit.only:仅运行当前测试/测试组
  • test.skipit.skip:跳过特定测试
  • describe.only / describe.skip:应用于测试组

便于调试。

善用 expect.assertions(number)

在异步测试中,确保期望的断言次数被执行,避免假阳性。

test('async test ensures both branches', async () => {
  expect.assertions(2);
  try {
    await someAsync();
  } catch (e) {
    expect(e).toBeInstanceOf(Error);
  }
  expect(true).toBe(true);
});

数据驱动测试

使用 test.each 减少重复代码:

test.each([
  [1, 1, 2],
  [1, 2, 3],
  [2, 1, 3],
])('.add(%i, %i) returns %i', (a, b, expected) => {
  expect(a + b).toBe(expected);
});

定时器模拟

使用 jest.useFakeTimers() 控制时间,避免等待真实延迟:

jest.useFakeTimers();

test('calls the callback after 1 second', () => {
  const callback = jest.fn();
  setTimeout(callback, 1000);

  // 快进 1 秒
  jest.advanceTimersByTime(1000);
  expect(callback).toHaveBeenCalled();
});

隔离文件与并发

Jest 默认并行运行测试文件,每个文件在一个独立的进程中运行,环境独立。如果需要串行执行,使用 --runInBand

集成到 CI/CD

在 CI 环境中使用 --ci 参数,Jest 将只运行一次测试,生成 JSON 覆盖率报告,便于集成到 Jenkins、GitHub Actions 等。

jest --ci --coverage --reporters=default --reporters=jest-junit