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

Практика

  1. Замокай fetch глобально и протестируй функцию getWeather(city)
  2. Используй jest.spyOn для перехвата console.error в обработчике ошибок
  3. Создай ручной мок для модуля localStoragemocks)
  4. Протестируй retry-функцию, которая вызывает мок-API 3 раза при ошибке

Связанные темы

Ресурсы