Перейти до основного змісту
Версія: Next

Ручні імітації

Ручні імітації використовуються як заглушки функціоналу імітаціями даних. Наприклад, замість того, щоб отримати доступ до віддаленого ресурсу на зразок вебсайту або бази даних, ви можете створити ручну імітацію, яка дозволить використання фіктивних даних. Це гарантує, що ваші тести будуть швидкими та однорідними.

Імітація модулів користувача

Ручні імітації визначаються шляхом запису модуля в підкаталог __mocks__/, прилеглий до цього модуля. Наприклад, для імітації модуля, що називається user, в каталозі models, треба створити файл під назвою user.js і додати його в каталог models/__mocks__.

caution

Каталог __mocks__ чутливий до регістру, тому іменування каталогу __MOCKS__ порушить роботу в деяких системах.

note

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

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').

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.

caution

Якщо нам треба імітувати вбудовані модулі Node (наприклад, fs або path), тоді явний виклик, наприклад, jest.mock('path'), є обов'язковим, бо вбудовані модулі за замовчуванням не імітуються.

Приклади

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

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.

info

Для повноцінної імітації, Jest потрібно, аби jest.mock('moduleName') був у тому ж контексті, що й твердження require/import.

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;

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. reateMockFromModule('fs');

// Це користувацька функція, яку наші тести можуть використовувати під час налаштування, щоб вказати
// вигляд файлів імітованої файлової системи, коли використовується будь-який з
// `fs` API.
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));
}
}

// Користувацька версія `readdirSync`, яка отримує дані зі спеціального імітованого
// списку файлів, створеного за допомогою __setMockFiles
function readdirSync(directoryPath) {
return mockFiles[directoryPath] || [];
}

fs.__setMockFiles = __setMockFiles;
fs.readdirSync = readdirSync;

module.exports = fs;

Now we write our test. В цьому випадку, 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(() => {
// Налаштування імітованої інформації деяких файлів перед кожним тестом
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);
});
});

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.

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.

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

Використання з імпортами модуля ES

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.

caution

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

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.

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(), // застарілий код
removeListener: jest.fn(), // застарілий код
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

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'; // Має бути імпортовано перед тестованим файлом
import {myMethod} from './file-to-test';

describe('myMethod()', () => {
// Тут проходитиме тестування методу...
});