跳至主要内容
版本:29.7

手动 Mock

手动 Mock 用于使用模拟数据来模拟功能。例如,您可能希望创建一个手动 Mock,而不是访问像网站或数据库这样的远程资源,以便使用假数据。这可以确保您的测试速度快且不会出现故障。

模拟用户模块

手动 Mock 是通过在与模块相邻的 __mocks__/ 子目录中编写模块来定义的。例如,要模拟 models 目录中的名为 user 的模块,请创建一个名为 user.js 的文件并将其放在 models/__mocks__ 目录中。

注意

__mocks__ 文件夹区分大小写,因此将目录命名为 __MOCKS__ 会在某些系统上导致故障。

提示

当我们在测试中需要该模块时(意味着我们想要使用手动 Mock 而不是实际实现),显式调用 jest.mock('./moduleName') 是**必需的**。

模拟 Node 模块

如果您要模拟的模块是 Node 模块(例如:lodash),则 Mock 应该放在与 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

当为给定模块存在手动 Mock 时,Jest 的模块系统将在显式调用 jest.mock('moduleName') 时使用该模块。但是,当 automock 设置为 true 时,即使没有调用 jest.mock('moduleName'),也会使用手动 Mock 实现而不是自动创建的 Mock。要选择退出此行为,您需要在应该使用实际模块实现的测试中显式调用 jest.unmock('moduleName')

信息

为了正确模拟,Jest 需要 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;

由于我们希望我们的测试避免实际访问磁盘(这很慢且脆弱),因此我们通过扩展自动 Mock 来为 fs 模块创建一个手动 Mock。我们的手动 Mock 将实现我们可以为测试构建的 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;

现在我们编写我们的测试。在这种情况下,必须显式调用 jest.mock('fs'),因为 fs 是 Node 的内置模块

__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);
});
});

此处显示的示例 Mock 使用 jest.createMockFromModule 生成自动 Mock,并覆盖其默认行为。这是推荐的方法,但完全是可选的。如果您不想使用自动 Mock,您可以从 Mock 文件中导出自己的函数。完全手动 Mock 的一个缺点是它们是手动的,这意味着您必须在模拟的模块发生更改时手动更新它们。因此,当它满足您的需求时,最好使用或扩展自动 Mock。

为了确保手动 Mock 及其实际实现保持同步,在您的手动 Mock 中使用 jest.requireActual(moduleName) 要求实际模块,并在导出它之前用 Mock 函数对其进行修改可能会有用。

此示例的代码可在 examples/manual-mocks 中找到。

与 ES 模块导入一起使用

如果您使用的是 ES 模块导入,那么您通常会倾向于将 import 语句放在测试文件的顶部。但通常您需要指示 Jest 在模块使用它之前使用 Mock。为此,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 会报告相同的错误。在这种情况下,解决方案是将手动 Mock 移动到单独的文件中,并在测试文件中**在**被测试文件之前包含此文件

import './matchMedia.mock'; // Must be imported before the tested file
import {myMethod} from './file-to-test';

describe('myMethod()', () => {
// Test the method here...
});