Jest 实战指南
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.json 的 scripts 中添加:
"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.js、jest.config.ts、jest.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 可查看详细的代码覆盖情况。
通过配置 collectCoverageFrom 和 coverageThreshold 可以精细控制范围和最低标准,结合 CI/CD 保证代码质量。
实战技巧与最佳实践
组织测试结构
使用 describe 和 it 替代 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.only或it.only:仅运行当前测试/测试组test.skip或it.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