Mock Functions
Mock functions (мок-функции) позволяют подменять реализацию, перехватывать вызовы и контролировать возвращаемые значения в тестах.
Зачем нужно
В реальном коде функции вызывают другие функции, API, БД. Мок-функции позволяют: заменить тяжёлую зависимость на лёгкую, проверить что функция была вызвана с нужными аргументами, контролировать что она возвращает.
Где используется
В unit-тестах для изоляции от зависимостей: HTTP-клиенты, база данных, логгеры, сторонние библиотеки.
Предпосылки
Моки и стабы, Matchers, Асинхронные тесты
jest.fn — создание мок-функции
Базовое использование
const mockFn = jest.fn;
mockFn('hello');
mockFn(42, true);
// Информация о вызовах
console.log(mockFn.mock.calls);
// [ ['hello'], [42, true] ]
console.log(mockFn.mock.calls.length); // 2
console.log(mockFn.mock.calls[0][0]); // 'hello'
console.log(mockFn.mock.calls[1]); // [42, true]
mockReturnValue — задать возвращаемое значение
const getPrice = jest.fn
.mockReturnValue(100);
console.log(getPrice); // 100
console.log(getPrice); // 100 — каждый раз одинаково
mockReturnValueOnce — разные значения при разных вызовах
const random = jest.fn
.mockReturnValueOnce(1)
.mockReturnValueOnce(2)
.mockReturnValueOnce(3)
.mockReturnValue(0); // для всех остальных
console.log(random); // 1
console.log(random); // 2
console.log(random); // 3
console.log(random); // 0
console.log(random); // 0
mockImplementation — своя реализация
const calculate = jest.fn
.mockImplementation((a, b) => a * b);
console.log(calculate(3, 4)); // 12
console.log(calculate(5, 6)); // 30
// mockImplementationOnce — разовая реализация
const fetch = jest.fn
.mockImplementationOnce( => Promise.resolve({ data: 'first' }))
.mockImplementationOnce( => Promise.reject(new Error('network')))
.mockImplementation( => Promise.resolve({ data: 'default' }));
mockResolvedValue / mockRejectedValue — для промисов
const fetchUser = jest.fn
.mockResolvedValue({ id: 1, name: 'Алиса' });
test('загружает пользователя', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Алиса');
});
const failingFetch = jest.fn
.mockRejectedValue(new Error('Сервер недоступен'));
test('обрабатывает ошибку', async () => {
await expect(failingFetch).rejects.toThrow('недоступен');
});
// Once-варианты
const api = jest.fn
.mockResolvedValueOnce({ ok: true })
.mockRejectedValueOnce(new Error('timeout'))
.mockResolvedValue({ ok: true });
jest.mock — мок целого модуля
Автоматический мок
// Jest автоматически заменяет все экспорты на jest.fn
jest.mock('./database');
const db = require('./database');
test('вызывает db.query', async () => {
db.query.mockResolvedValue([{ id: 1 }]);
const result = await db.query('SELECT * FROM users');
expect(result).toEqual([{ id: 1 }]);
expect(db.query).toHaveBeenCalledWith('SELECT * FROM users');
});
Частичный мок (mockImplementation в factory)
jest.mock('./utils', () => {
const actual = jest.requireActual('./utils'); // Оригинальный модуль
return {
...actual, // Все функции оставляем
fetchData: jest.fn, // Подменяем только fetchData
};
});
const { formatDate, fetchData } = require('./utils');
// formatDate — реальная функция
// fetchData — мок
Мок npm-пакета
jest.mock('axios');
const axios = require('axios');
test('отправляет GET-запрос', async () => {
axios.get.mockResolvedValue({
data: { users: },
status: 200,
});
const response = await axios.get('/api/users');
expect(response.status).toBe(200);
});
Ручной мок (файл в mocks)
src/
├── __mocks__/
│ └── axios.js ← Мок для axios
├── services/
│ ├── __mocks__/
│ │ └── api.js ← Мок для ./services/api
│ └── api.js
// __mocks__/axios.js
const axios = {
get: jest.fn.mockResolvedValue({ data: {} }),
post: jest.fn.mockResolvedValue({ data: {} }),
put: jest.fn.mockResolvedValue({ data: {} }),
delete: jest.fn.mockResolvedValue({ data: {} }),
create: jest.fn.mockReturnThis,
interceptors: {
request: { use: jest.fn },
response: { use: jest.fn },
},
};
module.exports = axios;
jest.spyOn — шпионить за методом
const mathService = {
add(a, b) { return a + b; },
multiply(a, b) { return a * b; },
};
test('шпионит за add', () => {
const spy = jest.spyOn(mathService, 'add');
const result = mathService.add(2, 3);
expect(spy).toHaveBeenCalledWith(2, 3);
expect(result).toBe(5); // Реальная реализация работает!
spy.mockRestore();
});
test('шпионит и подменяет', () => {
const spy = jest.spyOn(mathService, 'add')
.mockReturnValue(999);
const result = mathService.add(2, 3);
expect(result).toBe(999); // Подменённый результат
expect(spy).toHaveBeenCalledWith(2, 3);
spy.mockRestore();
});
spyOn для getters/setters
const config = {
_theme: 'dark',
get theme { return this._theme; },
set theme(val) { this._theme = val; },
};
test('шпионим за геттером', () => {
const spy = jest.spyOn(config, 'theme', 'get')
.mockReturnValue('light');
expect(config.theme).toBe('light');
spy.mockRestore();
});
Свойство .mock — вся информация
const fn = jest.fn.mockReturnValue('ok');
fn('a', 1);
fn('b', 2);
// fn.mock содержит:
fn.mock.calls; // [['a', 1], ['b', 2]]
fn.mock.results; // [{ type: 'return', value: 'ok' }, ...]
fn.mock.instances; // this для каждого вызова (для конструкторов)
fn.mock.lastCall; // ['b', 2]
Очистка, сброс, восстановление
const fn = jest.fn.mockReturnValue(42);
fn('test');
// mockClear — очищает записи вызовов
fn.mockClear();
console.log(fn.mock.calls.length); // 0
console.log(fn); // 42 — реализация сохранена
// mockReset — очищает записи + сбрасывает реализацию
fn.mockReset();
console.log(fn); // undefined
// mockRestore — для spyOn: восстанавливает оригинал
const spy = jest.spyOn(console, 'log');
spy.mockRestore(); // console.log снова настоящий
В afterEach
afterEach(() => {
jest.clearAllMocks; // Очистить вызовы всех моков
// jest.resetAllMocks; // + сбросить реализации
// jest.restoreAllMocks; // + восстановить оригиналы (spyOn)
});
Практический пример: тестирование сервиса
// userService.js
const db = require('./database');
const mailer = require('./mailer');
async function registerUser(name, email) {
const existing = await db.findByEmail(email);
if (existing) throw new Error('Email уже занят');
const user = await db.create({ name, email });
await mailer.sendWelcome(email, name);
return user;
}
module.exports = { registerUser };
// userService.test.js
jest.mock('./database');
jest.mock('./mailer');
const db = require('./database');
const mailer = require('./mailer');
const { registerUser } = require('./userService');
afterEach( => jest.clearAllMocks);
test('регистрирует нового пользователя', async () => {
db.findByEmail.mockResolvedValue(null);
db.create.mockResolvedValue({ id: 1, name: 'Алиса', email: 'a@t.com' });
mailer.sendWelcome.mockResolvedValue(true);
const user = await registerUser('Алиса', 'a@t.com');
expect(user.id).toBe(1);
expect(db.create).toHaveBeenCalledWith({
name: 'Алиса',
email: 'a@t.com',
});
expect(mailer.sendWelcome).toHaveBeenCalledWith('a@t.com', 'Алиса');
});
test('отклоняет дубликат email', async () => {
db.findByEmail.mockResolvedValue({ id: 1 });
await expect(
registerUser('Боб', 'existing@t.com')
).rejects.toThrow('уже занят');
expect(db.create).not.toHaveBeenCalled();
expect(mailer.sendWelcome).not.toHaveBeenCalled();
});
Частые ошибки
1. jest.mock после импорта
// ПЛОХО: мок не успевает подмениться
const db = require('./database');
jest.mock('./database'); // Слишком поздно!
// ХОРОШО: jest.mock поднимается (hoisting) автоматически
jest.mock('./database');
const db = require('./database');
2. Не очистили моки между тестами
// Тест 1 вызвал mockFn 3 раза
// Тест 2: expect(mockFn).toHaveBeenCalledTimes(1) → FAIL (вызвано 4 раза)
// Решение: afterEach( => jest.clearAllMocks);
3. mockRestore без spyOn
const fn = jest.fn;
fn.mockRestore(); // Ничего не делает для jest.fn
// mockRestore работает только для jest.spyOn
Практика
- Замокай
fetchглобально и протестируй функциюgetWeather(city) - Используй
jest.spyOnдля перехватаconsole.errorв обработчике ошибок - Создай ручной мок для модуля
localStorage(в mocks) - Протестируй retry-функцию, которая вызывает мок-API 3 раза при ошибке