Jest 扩展与插件 #
扩展概述 #
Jest 提供了丰富的扩展点,可以根据需求定制测试功能。
text
┌─────────────────────────────────────────────────────────────┐
│ Jest 扩展点 │
├─────────────────────────────────────────────────────────────┤
│ 1. 自定义匹配器 - 扩展断言功能 │
│ 2. 自定义报告器 - 定制测试报告 │
│ 3. 自定义运行器 - 改变测试执行方式 │
│ 4. 快照序列化器 - 定制快照格式 │
│ 5. 测试环境 - 定制测试环境 │
│ 6. 监视插件 - 扩展监视功能 │
└─────────────────────────────────────────────────────────────┘
自定义匹配器 #
基本匹配器 #
javascript
// matchers.js
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});
// 使用
test('within range', () => {
expect(5).toBeWithinRange(1, 10);
});
异步匹配器 #
javascript
expect.extend({
async toBeResolved(received) {
const pass = await received
.then(() => true)
.catch(() => false);
return {
pass,
message: () => pass
? 'expected promise to be rejected'
: 'expected promise to be resolved',
};
},
});
// 使用
test('async matcher', async () => {
await expect(Promise.resolve('value')).toBeResolved();
});
类型匹配器 #
javascript
expect.extend({
toBeType(received, expected) {
const actualType = typeof received;
const pass = actualType === expected;
return {
pass,
message: () => pass
? `expected ${received} not to be type ${expected}`
: `expected ${received} to be type ${expected}, got ${actualType}`,
};
},
});
// 使用
test('type matcher', () => {
expect('hello').toBeType('string');
expect(42).toBeType('number');
});
TypeScript 类型定义 #
typescript
// matchers.d.ts
declare global {
namespace jest {
interface Matchers<R> {
toBeWithinRange(floor: number, ceiling: number): R;
toBeType(expected: string): R;
toBeResolved(): Promise<R>;
}
}
}
export {};
自定义报告器 #
基本报告器 #
javascript
// customReporter.js
class CustomReporter {
constructor(globalConfig, options) {
this._globalConfig = globalConfig;
this._options = options;
}
onRunStart(results, options) {
console.log('Test run started');
}
onTestResult(test, testResult, results) {
console.log(`Test file: ${test.path}`);
console.log(`Passed: ${testResult.numPassingTests}`);
console.log(`Failed: ${testResult.numFailingTests}`);
}
onRunComplete(contexts, results) {
console.log('\nTest run completed');
console.log(`Total: ${results.numTotalTests}`);
console.log(`Passed: ${results.numPassedTests}`);
console.log(`Failed: ${results.numFailedTests}`);
}
}
module.exports = CustomReporter;
配置报告器 #
javascript
// jest.config.js
module.exports = {
reporters: [
'default',
['<rootDir>/customReporter.js', { option: 'value' }],
],
};
HTML 报告器 #
javascript
// htmlReporter.js
const fs = require('fs');
class HtmlReporter {
constructor(globalConfig, options) {
this._outputPath = options.outputPath || 'test-report.html';
}
onRunComplete(contexts, results) {
const html = this._generateHtml(results);
fs.writeFileSync(this._outputPath, html);
console.log(`Report generated: ${this._outputPath}`);
}
_generateHtml(results) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Test Report</title>
</head>
<body>
<h1>Test Report</h1>
<p>Total: ${results.numTotalTests}</p>
<p>Passed: ${results.numPassedTests}</p>
<p>Failed: ${results.numFailedTests}</p>
</body>
</html>
`;
}
}
module.exports = HtmlReporter;
自定义运行器 #
基本运行器 #
javascript
// customRunner.js
const { TestRunner } = require('jest-runner');
class CustomRunner extends TestRunner {
async runTests(tests, watcher, onStart, onResult, onFailure, options) {
// 自定义测试执行逻辑
console.log('Running tests with custom runner');
return await super.runTests(
tests,
watcher,
onStart,
onResult,
onFailure,
options
);
}
}
module.exports = CustomRunner;
配置运行器 #
javascript
// jest.config.js
module.exports = {
runner: './customRunner.js',
};
快照序列化器 #
自定义序列化器 #
javascript
// customSerializer.js
module.exports = {
test: (val) => val && val.isCustomType,
serialize: (val, config, indentation, depth, refs) => {
return `CustomType(${val.value})`;
},
};
配置序列化器 #
javascript
// jest.config.js
module.exports = {
snapshotSerializers: ['<rootDir>/customSerializer.js'],
};
Moment.js 序列化器 #
javascript
// momentSerializer.js
module.exports = {
test: (val) => val && val._isAMomentObject,
serialize: (val) => `Moment(${val.toISOString()})`,
};
Error 序列化器 #
javascript
// errorSerializer.js
module.exports = {
test: (val) => val instanceof Error,
serialize: (val) => `Error: ${val.message}`,
};
自定义测试环境 #
扩展 jsdom 环境 #
javascript
// customEnvironment.js
const JsdomEnvironment = require('jest-environment-jsdom');
class CustomEnvironment extends JsdomEnvironment {
constructor(config, context) {
super(config, context);
}
async setup() {
await super.setup();
// 添加自定义全局变量
this.global.myCustomGlobal = 'value';
// Mock localStorage
this.global.localStorage = {
getItem: jest.fn(),
setItem: jest.fn(),
};
}
async teardown() {
// 清理
await super.teardown();
}
}
module.exports = CustomEnvironment;
Node 环境扩展 #
javascript
// customNodeEnvironment.js
const NodeEnvironment = require('jest-environment-node');
class CustomNodeEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
// 设置环境变量
this.global.process.env.TEST_MODE = 'true';
// 添加全局工具
this.global.testUtils = {
createMockData: () => ({ id: 1 }),
};
}
}
module.exports = CustomNodeEnvironment;
监视插件 #
自定义监视插件 #
javascript
// customWatchPlugin.js
class CustomWatchPlugin {
constructor({ stdin, stdout }) {
this._stdin = stdin;
this._stdout = stdout;
}
apply(jestHooks) {
jestHooks.onFileChange(({ projects }) => {
console.log('Files changed');
});
}
getUsageInfo() {
return {
key: 'c',
prompt: 'run custom command',
};
}
run(globalConfig, updateConfigAndRun) {
console.log('Running custom command');
return Promise.resolve(true);
}
}
module.exports = CustomWatchPlugin;
配置监视插件 #
javascript
// jest.config.js
module.exports = {
watchPlugins: [
'<rootDir>/customWatchPlugin.js',
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
};
实用扩展示例 #
API 响应匹配器 #
javascript
expect.extend({
toBeValidApiResponse(received) {
const pass = received &&
typeof received === 'object' &&
'data' in received &&
'status' in received;
return {
pass,
message: () => pass
? 'expected response to be invalid'
: 'expected valid API response with data and status fields',
};
},
});
// 使用
test('API response', async () => {
const response = await fetchApi();
expect(response).toBeValidApiResponse();
});
组件状态匹配器 #
javascript
expect.extend({
toHaveState(received, state) {
const pass = received.state(state.key) === state.value;
return {
pass,
message: () => pass
? `expected component not to have state ${state.key} = ${state.value}`
: `expected component to have state ${state.key} = ${state.value}`,
};
},
});
时间匹配器 #
javascript
expect.extend({
toBeAfter(received, date) {
const pass = new Date(received) > new Date(date);
return {
pass,
message: () => pass
? `expected ${received} not to be after ${date}`
: `expected ${received} to be after ${date}`,
};
},
toBeBefore(received, date) {
const pass = new Date(received) < new Date(date);
return {
pass,
message: () => pass
? `expected ${received} not to be before ${date}`
: `expected ${received} to be before ${date}`,
};
},
});
发布扩展 #
npm 包结构 #
text
my-jest-matchers/
├── src/
│ ├── index.js
│ └── matchers/
│ ├── toBeWithinRange.js
│ └── toBeType.js
├── types/
│ └── index.d.ts
├── package.json
└── README.md
package.json #
json
{
"name": "my-jest-matchers",
"version": "1.0.0",
"main": "src/index.js",
"types": "types/index.d.ts",
"peerDependencies": {
"jest": ">=27.0.0"
},
"keywords": [
"jest",
"matchers",
"testing"
]
}
使用发布的扩展 #
javascript
// jest.setup.js
import 'my-jest-matchers';
最佳实践 #
1. 提供清晰的错误消息 #
javascript
expect.extend({
toBeValid(received) {
return {
pass: received.valid,
message: () => received.valid
? `expected ${received} to be invalid`
: `expected ${received} to be valid. Errors: ${received.errors.join(', ')}`,
};
},
});
2. 支持链式调用 #
javascript
expect.extend({
toBeValidEmail(received) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const pass = emailRegex.test(received);
return {
pass,
message: () => pass
? `expected ${received} not to be a valid email`
: `expected ${received} to be a valid email`,
};
},
});
// 支持 .not
expect('invalid').not.toBeValidEmail();
3. 提供类型定义 #
typescript
declare global {
namespace jest {
interface Matchers<R> {
toBeValidEmail(): R;
toBeValid(): R;
}
}
}
下一步 #
现在你已经掌握了 Jest 扩展与插件,接下来学习 React 测试 实战 React 组件测试!
最后更新:2026-03-28