跳至主内容
版本:下一篇

手动模拟

非官方测试版翻译

本页面由 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 模块。

FileSummarizer.js
'use strict';

const fs = require('fs');

function summarizeFilesInDirectorySync(directory) {
return fs.readdirSync(directory).map(fileName => ({
directory,
fileName,
}));
}

exports.summarizeFilesInDirectorySync = summarizeFilesInDirectorySync;

为避免测试实际访问磁盘(速度慢且不稳定),我们通过扩展自动模拟创建了 fs 模块的手动模拟。该手动模拟将实现自定义的 fs API 供测试使用:

__mocks__/fs.js
'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'):

__tests__/FileSummarizer-test.js
'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...
});