Skip to main content
Version: 29.7

手动模拟

手动模拟用于通过模拟数据来消除功能。例如,你可能希望创建一个允许你使用虚假数据的手动模拟,而不是访问网站或数据库等远程资源。这可以确保你的测试快速而不不稳定。

¥Manual mocks are used to stub out functionality with mock data. For example, instead of accessing a remote resource like a website or a database, you might want to create a manual mock that allows you to use fake data. This ensures your tests will be fast and not flaky.

模拟用户模块

¥Mocking user modules

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

¥Manual mocks are defined by writing a module in a __mocks__/ subdirectory immediately adjacent to the module. For example, to mock a module called user in the models directory, create a file called user.js and put it in the models/__mocks__ directory.

提醒

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

¥The __mocks__ folder is case-sensitive, so naming the directory __MOCKS__ will break on some systems.

注意

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

¥When we require that module in our tests (meaning we want to use the manual mock instead of the real implementation), explicitly calling jest.mock('./moduleName') is required.

模拟 Node 模块

¥Mocking Node modules

如果你正在模拟的模块是 Node 模块(例如:lodash),则模拟应放置在与 node_modules 相邻的 __mocks__ 目录中(除非你将 roots 配置为指向项目根目录以外的文件夹),并且将自动模拟。无需显式调用 jest.mock('module_name')

¥If the module you are mocking is a Node module (e.g.: lodash), the mock should be placed in the __mocks__ directory adjacent to node_modules (unless you configured roots to point to a folder other than the project root) and will be automatically mocked. There's no need to explicitly call jest.mock('module_name').

可以通过在与作用域模块名称匹配的目录结构中创建文件来模拟作用域模块(也称为 范围包)。例如,要模拟名为 @scope/project-name 的作用域模块,请在 __mocks__/@scope/project-name.js 处创建一个文件,并相应地创建 @scope/ 目录。

¥Scoped modules (also known as scoped packages) can be mocked by creating a file in a directory structure that matches the name of the scoped module. For example, to mock a scoped module called @scope/project-name, create a file at __mocks__/@scope/project-name.js, creating the @scope/ directory accordingly.

提醒

如果我们想模拟 Node 的内置模块(例如:fspath),那么显式调用例如 jest.mock('path') 是必需的,因为默认情况下不会模拟内置模块。

¥If we want to mock Node's built-in modules (e.g.: fs or path), then explicitly calling e.g. jest.mock('path') is required, because built-in modules are not mocked by default.

示例

¥Examples

.
├── config
├── __mocks__
│   └── fs.js
├── models
│   ├── __mocks__
│   │   └── user.js
│   └── user.js
├── node_modules
└── views

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

¥When a manual mock exists for a given module, Jest's module system will use that module when explicitly calling jest.mock('moduleName'). However, when automock is set to true, the manual mock implementation will be used instead of the automatically created mock, even if jest.mock('moduleName') is not called. To opt out of this behavior you will need to explicitly call jest.unmock('moduleName') in tests that should use the actual module implementation.

信息

为了正确模拟,Jest 需要 jest.mock('moduleName')require/import 语句位于同一范围内。

¥In order to mock properly, Jest needs jest.mock('moduleName') to be in the same scope as the require/import statement.

这是一个人为的示例,其中我们有一个模块提供给定目录中所有文件的摘要。在本例中,我们使用核心(内置)fs 模块。

¥Here's a contrived example where we have a module that provides a summary of all the files in a given directory. In this case, we use the core (built in) fs module.

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 的自定义版本,我们可以在其基础上进行测试:

¥Since we'd like our tests to avoid actually hitting the disk (that's pretty slow and fragile), we create a manual mock for the fs module by extending an automatic mock. Our manual mock will implement custom versions of the fs APIs that we can build on for our tests:

__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 的内置模块:

¥Now we write our test. In this case jest.mock('fs') must be called explicitly, because fs is Node’s built-in module:

__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 生成自动模拟,并覆盖其默认行为。这是推荐的方法,但完全是可选的。如果你根本不想使用自动模拟,你可以从模拟文件中导出你自己的函数。完全手动模拟的一个缺点是它们是手动的 - 这意味着只要它们正在模拟的模块发生变化,你就必须手动更新它们。因此,最好在满足你的需求时使用或扩展自动模拟。

¥The example mock shown here uses jest.createMockFromModule to generate an automatic mock, and overrides its default behavior. This is the recommended approach, but is completely optional. If you do not want to use the automatic mock at all, you can export your own functions from the mock file. One downside to fully manual mocks is that they're manual – meaning you have to manually update them any time the module they are mocking changes. Because of this, it's best to use or extend the automatic mock when it works for your needs.

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

¥To ensure that a manual mock and its real implementation stay in sync, it might be useful to require the real module using jest.requireActual(moduleName) in your manual mock and amending it with mock functions before exporting it.

此示例的代码可在 examples/manual-mocks 处获取。

¥The code for this example is available at examples/manual-mocks.

与 ES 模块导入一起使用

¥Using with ES module imports

如果你使用 ES 模块导入,那么你通常会倾向于将 import 语句放在测试文件的顶部。但通常你需要在模块使用模拟之前指示 Jest 使用模拟。因此,Jest 会自动将 jest.mock 调用提升到模块的顶部(在任何导入之前)。要了解有关此内容的更多信息并查看其实际情况,请参阅 这个仓库

¥If you're using ES module imports then you'll normally be inclined to put your import statements at the top of the test file. But often you need to instruct Jest to use a mock before modules use it. For this reason, Jest will automatically hoist jest.mock calls to the top of the module (before any imports). To learn more about this and see it in action, see this repo.

提醒

如果启用了 ECMAScript 模块支持,则 jest.mock 调用无法提升到模块顶部。ESM 模块加载器始终在执行代码之前评估静态导入。详情请参见 ECMAScript 模块

¥jest.mock calls cannot be hoisted to the top of the module if you enabled ECMAScript modules support. The ESM module loader always evaluates the static imports before executing code. See ECMAScriptModules for details.

JSDOM 中未实现的模拟方法

¥Mocking methods which are not implemented in JSDOM

如果某些代码使用了 JSDOM(Jest 使用的 DOM 实现)尚未实现的方法,则很难对其进行测试。这是例如 window.matchMedia() 的情况。Jest 返回 TypeError: window.matchMedia is not a function 并且无法正确执行测试。

¥If some code uses a method which JSDOM (the DOM implementation used by Jest) hasn't implemented yet, testing it is not easily possible. This is e.g. the case with window.matchMedia(). Jest returns TypeError: window.matchMedia is not a function and doesn't properly execute the test.

在这种情况下,在测试文件中模拟 matchMedia 应该可以解决问题:

¥In this case, mocking matchMedia in the test file should solve the issue:

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 会报同样的错误。在这种情况下,解决方案是将手动模拟移动到一个单独的文件中,并在测试中将其包含在测试文件之前:

¥This works if window.matchMedia() is used in a function (or method) which is invoked in the test. If window.matchMedia() is executed directly in the tested file, Jest reports the same error. In this case, the solution is to move the manual mock into a separate file and include this one in the test before the tested file:

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

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