Jest 手动模拟
手动模拟用于通过模拟数据来剔除功能。例如,你可能希望创建一个允许使用假数据的手动模拟,而不是访问像网站或数据库这样的远程资源。这将确保你的测试速度快,而且不会出现问题。
模拟用户模块
手动模拟是通过在__mocks__/
紧邻模块的子目录中编写模块来定义的。例如,mock 一个名为模块user
的models
目录下,创建一个名为user.js
,并把它的models/__mocks__
目录中。请注意,__mocks__
文件夹区分大小写,因此命名目录__MOCKS__
在某些系统上会中断。
当我们需要在我们的测试,模块,显式调用jest.mock('./moduleName')
是必需的。
模拟节点模块
如果lodash
模拟的模块是 Node 模块(例如:),则模拟应放置在__mocks__
相邻的目录中node_modules
(除非你配置roots为指向项目根目录以外的文件夹),并且会自动模拟。无需显式调用jest.mock('module_name')
.
可以通过在与作用域模块名称匹配的目录结构中创建文件来模拟作用域模块。例如,要模拟名为@scope/project-name
的作用域模块,请在__mocks__/@scope/project-name.js
处创建一个文件,并相应地创建@scope/
目录。
提示:如果我们想模拟节点的核心模块(例如:fs
或path
),则显式调用例如jest.mock('path')
是需要的,因为核心节点模块默认情况下不会被模拟。
例子
.
├── config
├── __mocks__
│ └── fs.js
├── models
│ ├── __mocks__
│ │ └── user.js
│ └── user.js
├── node_modules
└── views
当给定模块存在手动模拟时,Jest 的模块系统将在显式调用jest.mock('moduleName')
时使用该模块. 但是,当automock
设置为时true
,即使jest.mock('moduleName')
没有调用,也会使用手动模拟实现而不是自动创建的模拟。要选择退出此行为,需要显式调用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;
由于我们希望我们的测试避免实际访问磁盘(这非常缓慢和脆弱),我们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 模块:
// __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
调用提升到模块的顶部(在任何导入之前)。要了解有关此内容的更多信息并查看其实际效果,请参阅此内容。
未在 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...
});
更多建议: