Jest 深入解析
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 将常用的测试函数注入为全局变量,你无需在测试文件中逐个引入。
test 与 it
两者完全等价,it 是 test 的别名,用于更接近行为描述风格的写法。
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' });
真值判断
toBeNull、toBeUndefined、toBeDefined、toBeTruthy、toBeFalsy。
数字比较
toBeGreaterThan、toBeLessThan、toBeCloseTo(浮点近似比较)。
字符串
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.instances:new实例化时的this上下文
便捷添加:
mockReturnValue/mockReturnValueOncemockImplementation/mockImplementationOncemockResolvedValue/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.js、jest.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.only或it.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',
};