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

В Jasmine отслеживание вызовов функций реализуется через шпионы (spies). Шпион — это объект, который перехватывает вызовы функции, записывает параметры, с которыми она была вызвана, количество вызовов, а при необходимости позволяет контролировать возвращаемые значения или поведение функции.

Создание шпионов

Существует несколько способов создания шпионов:

  1. Шпион на существующую функцию объекта:
const calculator = {
    add: (a, b) => a + b
};

spyOn(calculator, 'add');

calculator.add(2, 3);

expect(calculator.add).toHaveBeenCalled();
expect(calculator.add).toHaveBeenCalledWith(2, 3);
  1. Шпион-функция без существующей реализации:
const callback = jasmine.createSpy('callback');

callback('test');

expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith('test');
  1. Шпион с возвращаемым значением или поведением:
const fetchData = jasmine.createSpy('fetchData').and.returnValue(Promise.resolve('data'));

fetchData().then(result => {
    expect(result).toBe('data');
});

Также доступны методы and.callFake для определения собственной функции и and.throwError для имитации ошибок.


Проверка вызовов и параметров

Jasmine предоставляет мощный API для анализа шпионов:

  • toHaveBeenCalled — проверяет, был ли вызов.
  • toHaveBeenCalledTimes(n) — проверяет точное количество вызовов.
  • toHaveBeenCalledWith(…args) — проверяет, вызывалась ли функция с конкретными аргументами.
  • toHaveBeenCalledOnceWith(…args) — проверка однократного вызова с заданными параметрами.
  • calls.mostRecent() — возвращает объект с информацией о последнем вызове.
  • calls.first() — объект с данными о первом вызове.
  • calls.all() — массив всех вызовов.

Пример:

const logger = jasmine.createSpy('logger');

logger('start');
logger('process');
logger('end');

expect(logger).toHaveBeenCalledTimes(3);
expect(logger.calls.all()[1].args).toEqual(['process']);

Встроенные свойства шпионов

Каждый шпион содержит объект calls, который предоставляет детальную статистику:

  • calls.any() — возвращает true, если шпион вызывался хотя бы один раз.
  • calls.count() — количество вызовов.
  • calls.argsFor(index) — массив аргументов для вызова с указанным индексом.
  • calls.allArgs() — массив аргументов всех вызовов.
  • calls.all() — массив объектов вызовов, включая контекст this.
  • calls.reset() — сбрасывает историю вызовов шпиона.

Пример использования calls:

const math = jasmine.createSpyObj('math', ['multiply']);
math.multiply(2, 3);
math.multiply(4, 5);

expect(math.multiply.calls.count()).toBe(2);
expect(math.multiply.calls.argsFor(0)).toEqual([2, 3]);
math.multiply.calls.reset();
expect(math.multiply.calls.any()).toBeFalse();

Шпионы на объекты и методы

Jasmine позволяет создавать шпионы на все методы объекта одновременно:

const api = {
    getUser: () => {},
    saveUser: () => {}
};

const apiSpy = jasmine.createSpyObj('api', ['getUser', 'saveUser']);

apiSpy.getUser('Alice');
expect(apiSpy.getUser).toHaveBeenCalledWith('Alice');

Для каждого метода можно отдельно определить возвращаемое значение:

apiSpy.getUser.and.returnValue({ name: 'Alice' });
expect(apiSpy.getUser()).toEqual({ name: 'Alice' });

Изменение поведения шпиона

Шпион может:

  • Вызвать оригинальную функцию: and.callThrough()
  • Возвращать конкретное значение: and.returnValue(value)
  • Использовать кастомную реализацию: and.callFake(fn)
  • Бросать ошибку: and.throwError(message)

Пример использования callThrough:

const calculator = {
    multiply: (a, b) => a * b
};

spyOn(calculator, 'multiply').and.callThrough();
expect(calculator.multiply(2, 3)).toBe(6);
expect(calculator.multiply).toHaveBeenCalledWith(2, 3);

Проверка последовательности вызовов

Jasmine позволяет контролировать порядок вызовов функций, используя массив объектов вызовов:

const a = jasmine.createSpy('a');
const b = jasmine.createSpy('b');

a();
b();

expect(a.calls.first().invocationOrder).toBeLessThan(b.calls.first().invocationOrder);

Это полезно для тестирования сложных процессов, где важен порядок операций.


Практические рекомендации

  • Использовать spyOn для методов объектов, чтобы сохранить возможность контроля оригинальной реализации.
  • Применять createSpy для независимых функций обратного вызова.
  • Всегда сбрасывать вызовы шпионов между тестами (calls.reset()), если шпион используется повторно.
  • При проверке сложных аргументов использовать toHaveBeenCalledWith(jasmine.objectContaining({...})) для частичной проверки объекта.
  • Шпионы подходят не только для синхронного кода, но и для асинхронных функций, включая промисы и async/await.

Отслеживание вызовов функций с помощью шпионов Jasmine обеспечивает точный контроль за поведением кода и позволяет создавать надежные и детальные юнит-тесты, фиксируя не только результат, но и всю динамику вызовов функций.