jest.spyOn для шпионажа за функциями

jest.spyOn используется для шпионажа за существующими функциями или методами объектов без их полного подменивания. В контексте React Testing Library этот инструмент особенно важен при тестировании побочных эффектов, взаимодействия с внешними модулями, браузерными API и колбэками, которые не передаются напрямую в компонент.

В отличие от jest.fn(), который создаёт новую функцию, jest.spyOn работает только с уже существующим методом объекта, сохраняя его оригинальную реализацию (если явно не переопределить поведение).


Базовый синтаксис и поведение

jest.spyOn(object, 'methodName')

После вызова:

  • метод начинает отслеживаться Jest
  • можно проверять количество вызовов, аргументы, контекст
  • оригинальная реализация по умолчанию сохраняется

Пример:

const math = {
  sum(a, b) {
    return a + b;
  }
};

const spy = jest.spyOn(math, 'sum');

math.sum(2, 3);

expect(spy).toHaveBeenCalledWith(2, 3);
expect(spy).toHaveBeenCalledTimes(1);

Шпионаж без подмены логики

Ключевая особенность jest.spyOn — возможность наблюдать поведение без вмешательства.

jest.spyOn(console, 'log');

console.log('Hello');

expect(console.log).toHaveBeenCalledWith('Hello');

В этом примере вывод в консоль реально происходит, но одновременно фиксируется факт вызова.


Подмена реализации через mockImplementation

При необходимости оригинальное поведение можно временно заменить:

jest.spyOn(api, 'fetchUser').mockImplementation(() => {
  return Promise.resolve({ name: 'Test' });
});

После этого:

  • метод остаётся тем же по имени и сигнатуре
  • реальный код не выполняется
  • появляется полный контроль над возвращаемым значением

Частый сценарий — подавление побочных эффектов:

jest.spyOn(window, 'alert').mockImplementation(() => {});

Использование в тестах React-компонентов

Отслеживание вызова внешней функции

import { render, fireEvent } from '@testing-library/react';
import Button from './Button';
import * as analytics from './analytics';

jest.spyOn(analytics, 'trackClick');

test('отправляется событие аналитики', () => {
  render(<Button />);

  fireEvent.click(screen.getByRole('button'));

  expect(analytics.trackClick).toHaveBeenCalled();
});

Здесь jest.spyOn позволяет проверить побочный эффект, не вмешиваясь в сам компонент.


Шпионаж за методами классов

class UserService {
  login() {
    return true;
  }
}

const service = new UserService();
jest.spyOn(service, 'login');

service.login();

expect(service.login).toHaveBeenCalled();

Важно: spyOn работает с конкретным экземпляром, если метод не является статическим.


Шпионаж за прототипами

Для отслеживания вызовов методов во всех экземплярах:

jest.spyOn(UserService.prototype, 'login');

Этот приём применяется при тестировании компонентов, которые создают объекты внутри себя.


Работа с асинхронными функциями

jest.spyOn корректно отслеживает асинхронные методы:

jest.spyOn(api, 'loadData').mockResolvedValue({ items: [] });

await api.loadData();

expect(api.loadData).toHaveBeenCalled();

Можно использовать:

  • mockResolvedValue
  • mockRejectedValue
  • mockImplementation(async () => ...)

Восстановление оригинальной реализации

После тестов шпионы обязательно очищаются, иначе возможны побочные эффекты между тестами.

Восстановление конкретного шпиона

const spy = jest.spyOn(console, 'error');

spy.mockRestore();

Глобальное восстановление

afterEach(() => {
  jest.restoreAllMocks();
});

mockRestore:

  • возвращает оригинальный метод
  • удаляет все счётчики вызовов

Отличие от jest.mock

Критерий jest.spyOn jest.mock
Работает с существующей функцией Да Нет
Сохраняет реализацию по умолчанию Да Нет
Подходит для частичного контроля Да Нет
Изолирует модуль полностью Нет Да

jest.spyOn применяется там, где важно проверить факт вызова, а не заменить весь модуль.


Типичные ошибки и ограничения

Нельзя шпионить за:

  • функциями, не являющимися свойствами объекта
  • стрелочными функциями, объявленными в модуле без экспорта
// Нельзя
jest.spyOn(doSomething, 'call');

Метод должен существовать на момент вызова spyOn, иначе тест упадёт с ошибкой.


Практический паттерн для React Testing Library

describe('Component with side effect', () => {
  const spy = jest.spyOn(service, 'send');

  afterEach(() => {
    spy.mockClear();
  });

  test('вызывается отправка данных', () => {
    render(<Component />);
    fireEvent.click(screen.getByText('Submit'));

    expect(spy).toHaveBeenCalled();
  });
});

Этот подход:

  • сохраняет читаемость теста
  • не нарушает архитектуру компонента
  • позволяет тестировать поведение, а не реализацию

Роль jest.spyOn в философии RTL

React Testing Library ориентирован на тестирование поведения пользователя, а не внутренней реализации компонентов. jest.spyOn используется точечно, когда необходимо подтвердить внешний эффект:

  • вызов API
  • логирование
  • аналитика
  • интеграция с браузерными API

Инструмент не противоречит философии RTL, если применяется вне DOM-проверок и не заставляет тест зависеть от внутренней структуры компонента.