手动模拟
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
手动模拟用于使用模拟数据替代实际功能。例如,与其访问远程资源(如网站或数据库),您可以创建手动模拟来使用虚假数据。这能确保测试快速运行且结果稳定。
模拟用户模块
手动模拟通过在模块同级目录下创建 __mocks__/ 子目录来定义。例如,要模拟 models 目录中的 user 模块,需创建 user.js 文件并置于 models/__mocks__ 目录中。
__mocks__ 文件夹名称区分大小写,使用 __MOCKS__ 等名称可能导致某些系统运行失败。
在测试中引入模块时(即希望使用手动模拟而非真实实现),必须显式调用 jest.mock('./moduleName')。
模拟 Node 模块
若要模拟 Node 模块(如 lodash),应将模拟文件置于与 node_modules 同级的 __mocks__ 目录中(除非配置了 roots 指向非项目根目录)。此类模块会被自动模拟,无需显式调用 jest.mock('module_name')。
作用域模块(亦称作用域包)可通过创建匹配其名称的目录结构来模拟。例如模拟 @scope/project-name 时,需创建 __mocks__/@scope/project-name.js 文件,并建立对应的 @scope/ 目录。
模拟 Node 内置模块(如 fs 或 path)时必须显式调用 jest.mock('path'),因为内置模块默认不会被模拟。
示例
.
├── config
├── __mocks__
│ └── fs.js
├── models
│ ├── __mocks__
│ │ └── user.js
│ └── user.js
├── node_modules
└── views
当存在某模块的手动模拟时,显式调用 jest.mock('moduleName') 会使 Jest 模块系统使用该模拟。但当 automock 设为 true 时,即使未调用 jest.mock('moduleName'),也会优先使用手动模拟而非自动生成的模拟。若需使用真实模块实现,应在测试中显式调用 jest.unmock('moduleName')。
为实现正确模拟,jest.mock('moduleName') 必须与 require/import 语句处于相同作用域。
以下示例展示了一个模块:该模块提供指定目录的文件摘要功能,其中使用了核心(内置)fs 模块。
'use strict';
const fs = require('fs');
function summarizeFilesInDirectorySync(directory) {
return fs.readdirSync(directory).map(fileName => ({
directory,
fileName,
}));
}
exports.summarizeFilesInDirectorySync = summarizeFilesInDirectorySync;
为避免测试实际访问磁盘(速度慢且不稳定),我们通过扩展自动模拟创建了 fs 模块的手动模拟。该手动模拟将实现自定义的 fs API 供测试使用:
'use strict';
const path = require('path');
const fs = jest.createMockFromModule('fs');
// This is a custom function that our tests can use during setup to specify
// what the files on the "mock" filesystem should look like when any of the
// `fs` APIs are used.
let mockFiles = Object.create(null);
function __setMockFiles(newMockFiles) {
mockFiles = Object.create(null);
for (const file in newMockFiles) {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
mockFiles[dir] = [];
}
mockFiles[dir].push(path.basename(file));
}
}
// A custom version of `readdirSync` that reads from the special mocked out
// file list set via __setMockFiles
function readdirSync(directoryPath) {
return mockFiles[directoryPath] || [];
}
fs.__setMockFiles = __setMockFiles;
fs.readdirSync = readdirSync;
module.exports = fs;
现在编写测试代码。由于 fs 是 Node 内置模块,此处必须显式调用 jest.mock('fs'):
'use strict';
jest.mock('fs');
describe('listFilesInDirectorySync', () => {
const MOCK_FILE_INFO = {
'/path/to/file1.js': 'console.log("file1 contents");',
'/path/to/file2.txt': 'file2 contents',
};
beforeEach(() => {
// Set up some mocked out file info before each test
require('fs').__setMockFiles(MOCK_FILE_INFO);
});
test('includes all files in the directory in the summary', () => {
const FileSummarizer = require('../FileSummarizer');
const fileSummary =
FileSummarizer.summarizeFilesInDirectorySync('/path/to');
expect(fileSummary.length).toBe(2);
});
});
本例使用 jest.createMockFromModule 生成自动模拟并覆盖其默认行为,这是推荐做法(但非强制)。若不想使用自动模拟,可直接从模拟文件导出自定义函数。完全手动模拟的缺点是需随被模拟模块变更而手动更新,因此建议优先 采用或扩展自动模拟方案。
为确保手动模拟与其实际实现保持同步,可以在手动模拟中使用 jest.requireActual(moduleName) 引入真实模块,并在导出前使用模拟函数对其进行修改。
此示例代码位于 examples/manual-mocks。
与 ES 模块导入一起使用
如果使用 ES 模块导入,通常会倾向于将 import 语句放在测试文件的顶部。但通常需要在模块使用模拟前告知 Jest,因此 Jest 会自动将 jest.mock 调用提升到模块顶部(在任何导入之前)。要了解更多信息并查看实际示例,请参阅此仓库。
如果启用了 ECMAScript 模块支持,则 jest.mock 调用无法被提升到模块顶部。ESM 模 块加载器总是在执行代码前先解析静态导入。详见 ECMAScriptModules。
模拟 JSDOM 中未实现的方法
如果代码使用了 JSDOM(Jest 使用的 DOM 实现)尚未实现的方法,则难以进行测试。例如 window.matchMedia() 就属于这种情况。Jest 会返回 TypeError: window.matchMedia is not a function 错误,导致测试无法正常执行。
这种情况下,在测试文件中模拟 matchMedia 可以解决问题:
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
这种方法适用于 window.matchMedia() 在测试调用的函数(或方法)中使用的情况。如果 window.matchMedia() 直接在测试文件中执行,Jest 仍会报相同错误。此时的解决方案是将手动模拟移到单独文件中,并在测试中先于被测试文件引入此模拟:
import './matchMedia.mock'; // Must be imported before the tested file
import {myMethod} from './file-to-test';
describe('myMethod()', () => {
// Test the method here...
});