Jest моки и стабы

Mock и stub в Jest — инструменты замены реальных зависимостей (функций, модулей, HTTP-запросов) контролируемыми тестовыми дублёрами для изоляции тестируемого кода.

Зачем нужно

Без мокирования unit-тест превращается в integration-тест: он стучится в БД, делает HTTP-запросы, пишет файлы — становится медленным и нестабильным. Mock позволяет проверить, как код реагирует на конкретный ответ зависимости (успех, ошибку, пустой результат), без запуска реальной инфраструктуры. Spy фиксирует, с какими аргументами вызвалась функция.

Где используется

  • Замена HTTP-клиента (axios, fetch) в unit-тестах сервисов
  • Замена модулей файловой системы (fs), БД-клиентов, email-сервисов
  • Проверка, что колбэк был вызван с нужными аргументами
  • Тестирование обработки ошибок — mock бросает исключение, тест проверяет поведение

Основной контент

jest.fn — функция-mock

test('вызывает колбэк с правильным аргументом', () => {
  const callback = jest.fn;

  [1, 2, 3].forEach(callback);

  expect(callback).toHaveBeenCalledTimes(3);
  expect(callback).toHaveBeenCalledWith(1, 0, [1, 2, 3]); // (value, index, array)
  expect(callback).toHaveBeenLastCalledWith(3, 2, [1, 2, 3]);
});

Настройка возвращаемого значения

const mockFn = jest.fn;

// Вернуть значение один раз
mockFn.mockReturnValueOnce(42);
mockFn.mockReturnValueOnce(99);
mockFn; // → 42
mockFn; // → 99
mockFn; // → undefined

// Всегда возвращать одно значение
mockFn.mockReturnValue('default');

// Промис
mockFn.mockResolvedValue({ id: 1, name: 'Alice' });
mockFn.mockRejectedValue(new Error('Not found'));

Мокирование модуля целиком

// emailService.js
const nodemailer = require('nodemailer');

async function sendWelcome(email) {
  const transport = nodemailer.createTransport({ /* ... */ });
  await transport.sendMail({ to: email, subject: 'Welcome' });
}
module.exports = { sendWelcome };
// emailService.test.js
jest.mock('nodemailer');
const nodemailer = require('nodemailer');
const { sendWelcome } = require('./emailService');

test('отправляет welcome-письмо', async () => {
  const mockSendMail = jest.fn.mockResolvedValue({ messageId: '123' });
  nodemailer.createTransport.mockReturnValue({ sendMail: mockSendMail });

  await sendWelcome('user@example.com');

  expect(mockSendMail).toHaveBeenCalledWith(
    expect.objectContaining({ to: 'user@example.com' })
  );
});

jest.spyOn — шпион на существующий метод

const fs = require('fs');

test('логирует в файл', () => {
  const spy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});

  writeLog('error', 'Something went wrong');

  expect(spy).toHaveBeenCalledWith(
    expect.stringContaining('app.log'),
    expect.stringContaining('Something went wrong')
  );

  spy.mockRestore(); // восстановить оригинальный метод
});

Мокирование fetch / axios

// Мок для fetch (глобальный)
global.fetch = jest.fn.mockResolvedValue({
  ok: true,
  json: jest.fn.mockResolvedValue({ id: 1, name: 'Alice' }),
});

test('загружает пользователя', async () => {
  const user = await getUser(1);
  expect(user.name).toBe('Alice');
  expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');
});
// Мок для axios
jest.mock('axios');
const axios = require('axios');

test('обрабатывает ошибку сети', async () => {
  axios.get.mockRejectedValue(new Error('Network Error'));
  await expect(fetchData).rejects.toThrow('Network Error');
});

Очистка между тестами

afterEach(() => {
  jest.clearAllMocks;  // сбросить счётчики вызовов и аргументы
  // jest.resetAllMocks;  // + убрать mockReturnValue
  // jest.restoreAllMocks; // + восстановить спаи (spyOn)
});

Частые ошибки

  • Мокировать после requirejest.mock вызывается до импорта модуля (Jest поднимает его в начало); порядок в файле не важен, но важно не забыть вызов
  • Не восстанавливать spyOn — без spy.mockRestore() или jest.restoreAllMocks оригинальный метод остаётся подменённым в следующих тестах
  • mockReturnValue vs mockResolvedValue — для async-функций нужен mockResolvedValue, иначе вернётся не промис, а объект {then: ...}
  • Проверять факт вызова вместо результата — если можно проверить результат функции — проверяй результат; mock-проверка «был ли вызван» — запасной вариант
  • Слишком широкие мокиjest.mock('./db') мокирует весь модуль; если нужна только одна функция — используй jest.spyOn

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

Ресурсы