mock – это особый тип функции, который позволяет временно переопределить реализацию одной функции, класса или модуля, чтобы придать ему другое поведение, чтобы протестировать его в отрыве от внешних зависимостей. вам отслеживать, сколько раз он был вызван, с какими параметрами и что он вернул.
Насмешка над одной функцией
Рассмотрим следующую функцию proxy()
.
function proxy(data, callback) { return callback(data); } module.exports = proxy;
Тестирование исполнения
Чтобы подтвердить, что функция callback()
действительно вызывается при выполнении функции proxy()
, мы можем:
- Создайте макет функции
callback()
, используя функциюjest.fn()
. - Выполните функцию
proxy()
, используя макетmockFn
. - Проверьте, был ли вызван макет, сколько раз он был вызван и с какими параметрами, используя следующие самоописательные средства сопоставления.
const proxy = require('./proxy'); test('it should invoke the callback function', () => { // (1) const mockFn = jest.fn(); // (2) proxy('Hello World', mockFn); // (3) expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenCalledWith('Hello World'); });
Переопределение реализации
Чтобы переопределить реализацию функции и, следовательно, ее поведение, мы можем определить ее новую реализацию внутри самой функции jest.fn()
и проверить ее результат с помощью сопоставителя toHaveReturnedWith()
.
const proxy = require('./proxy'); test('it should return the data length', () => { const mockFn = jest.fn(data => data && data.length || 0); proxy('Hello World', mockFn); expect(mockFn).toHaveReturnedWith(12); });
Переопределение возвращаемого значения
Чтобы переопределить возвращаемое значение функции вместо ее реализации, мы можем использовать функцию mockReturnValue()
, которая будет возвращать одно и то же значение для каждого вызова.
const proxy = require('./proxy'); test('it should return the data length', () => { const mockFn = jest.fn().mockReturnValue(20); proxy('Hello world', mockFn); expect(mockFn).toHaveReturnedWith(20); proxy('Bonjour le monde', mockFn); expect(mockFn).toHaveReturnedWith(20); });
Или мы можем использовать функцию mockReturnValueOnce()
, которая будет возвращать определенное значение только для следующего вызова.
test('it should return different data lengths', () => { const mockFn = jest.fn() .mockReturnValueOnce(3) .mockReturnValueOnce(5); proxy('Hello', mockFn); expect(mockFn).toHaveReturnedWith(3); proxy('Hello', mockFn); expect(mockFn).toHaveReturnedWith(5); });
Насмешка над классом
Давайте рассмотрим следующий класс User
, конструктор которого принимает в качестве аргумента строку, представляющую имя, и реализует функцию hello()
, которая возвращает строку, содержащую свойство name
.
class User { constructor(name) { this.name = name; } hello() { return "Hello " + this.name; } } module.exports = User;
Издевательство над целым классом
Поскольку классы ES6 по сути являются функциями-конструкторами с некоторым синтаксическим сахаром, любой макет для класса ES6 должен быть функцией или фактическим классом ES6 (что, опять же, является другой функцией), и, следовательно, может быть смоделирован с помощью функции jest.fn()
.
Чтобы смоделировать весь класс, мы можем использовать функцию jest.mock()
, которая принимает в качестве аргумента путь к модулю, для которого мы хотим создать макет, и функцию обратного вызова, которая возвращает макет, созданный с помощью jest.fn()
.
const MyClass = require('./myclass'); jest.mock('./myclass', () => { return jest.fn(() => { return { // ... }; }); });
Как вы можете видеть в этом примере, если мы имитируем класс User
следующим образом и создаем экземпляр нового объекта, используя строку Jack
, значение, содержащееся в свойстве name
, на самом деле будет тем, которое определено в макете, а не тем, которое передано в конструктор.
const User = require('./user'); jest.mock('./user', () => { return jest.fn(() => { return { name: 'John', hello: jest.fn() }; }); }); test('it should mock the User constructor', () => { const user = new User('Jack'); expect(user.name).toBe('John'); });
В качестве альтернативы мы также можем имитировать класс для каждого теста — аналогично функциям — используя метод mockImplementationOnce()
следующим образом.
const User = require('./user'); jest.mock('./user'); test('it should mock the User constructor', () => { User.mockImplementationOnce(() => { return { name: 'John', hello: jest.fn() }; }); const user = new User('Jack'); expect(user.name).toBe('John'); });
Шпионить за методом класса
Чтобы протестировать поведение класса без изменения его фактической реализации, мы можем использовать функцию jest.spyOn()
, которая принимает в качестве аргумента объект и метод для отслеживания вызовов и возвращает фиктивную функцию.
const User = require('./user'); test('it should return the string "Hello John"', () => { const user = new User('John'); const mock = jest.spyOn(User.prototype, 'hello'); user.hello(); expect(mock).toHaveReturnedWith("Hello John"); });
Насмешка над методом класса
Чтобы имитировать конкретный метод класса без изменения каких-либо других свойств, мы можем использовать функцию jest.spyOn()
, которую мы только что видели, и связать ее с методом .mockImplementationOnce()
.
const User = require('./user'); test('it should return the string "Hello John"', () => { const user = new User('John'); const mock = jest .spyOn(User.prototype, 'hello') .mockImplementationOnce(() => { return "Gutten Tag John"; }); user.hello(); expect(mock).toHaveReturnedWith("Gutten Tag John"); });
Издевательство над модулем
Создание макета всего модуля на самом деле похоже на создание макета класса, поскольку это можно сделать непосредственно в функции обратного вызова функции jest.mock()
.
jest.mock('module', () => { return jest.fn(() => { return { // ... }; }); });
Давайте рассмотрим следующий модуль, который создает экземпляр нового обработчика базы данных, используя класс Sequelize
, и выполняет попытку подключения к базе данных, используя метод authenticate()
объекта обработчика базы данных.
const Sequelize = require('sequelize'); async function connect(env) { const db = new Sequelize(env.name, env.user, env.password, { host: env.host, port: env.port, dialect: env.dialect, logging: env.logging }); await db.authenticate(); return db; }; module.exports = connect;
Чтобы утверждать, что функция connect
вызывает класс Sequelize
и метод authenticate()
, фактически не выполняя попытку соединения, мы можем имитировать модуль Sequelize
следующим образом:
const Sequelize = require('sequelize'); const connect = require('./connect'); jest.mock('sequelize', () => { return jest.fn(() => { return { authenticate: jest.fn() }; }); }); afterEach(() => jest.clearAllMocks()); const env = { name: 'database', user: 'john', password: 'hello', host: '127.0.0.1', port: 3306, dialect: 'mysql', logging: true }; test('it should invoke the Sequelize constructor', async () => { await connect(env); expect(Sequelize).toHaveBeenCalledWith(env.name, env.user, env.password, { host: env.host, port: env.port, dialect: env.dialect, logging: env.logging }); }); test('it should invoke the authenticate handler', async () => { const db = await connect(env); expect(db.authenticate).toHaveBeenCalledTimes(1); });
Хотите узнать больше?
Узнайте, как создавать реальные API в Node.js от первой строки кода до последней строки документации, из книги Build Layered Microservices, доступной по адресу learnbackend.dev.
Дополнительные материалы на PlainEnglish.io.
Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .
Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.