跳至主内容
版本:下一篇

手动模拟

非官方测试版翻译

本页面由 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 内置模块(如 fspath)时必须显式调用 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...
});