Перейти к содержимому

9. Spies и Stubs

Spies

Spy оборачивает реальную функцию, позволяя следить за вызовами, но сохраняя оригинальное поведение.

import { jest } from '@jest/globals'; // или vi from 'vitest'
// Базовый spy
const arr = [1, 2, 3];
const spy = jest.spyOn(arr, 'push');
arr.push(4); // реальный push выполнится
expect(spy).toHaveBeenCalledWith(4);
expect(arr).toEqual([1, 2, 3, 4]); // данные изменились по-настоящему
spy.mockRestore(); // восстановить оригинал
class Logger {
log(message) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
error(message) {
console.error(`ERROR: ${message}`);
}
}
class UserService {
constructor(db, logger) {
this.db = db;
this.logger = logger;
}
async createUser(userData) {
this.logger.log(`Creating user: ${userData.email}`);
const user = await this.db.insert(userData);
this.logger.log(`User created: ${user.id}`);
return user;
}
}
// Тест
test('logs creation events', async () => {
const logger = new Logger();
const db = { insert: jest.fn().mockResolvedValue({ id: 1, email: '[email protected]' }) };
const logSpy = jest.spyOn(logger, 'log');
const service = new UserService(db, logger);
await service.createUser({ email: '[email protected]' });
expect(logSpy).toHaveBeenCalledTimes(2);
expect(logSpy).toHaveBeenNthCalledWith(1, 'Creating user: [email protected]');
expect(logSpy).toHaveBeenNthCalledWith(2, expect.stringContaining('User created'));
logSpy.mockRestore();
});
// Spy на console
test('logs error on failure', async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
await processWithLogging(() => { throw new Error('Failed'); });
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed')
);
consoleSpy.mockRestore();
});
// Spy на Date
test('uses current date', () => {
const fixedDate = new Date('2024-01-15');
jest.spyOn(global, 'Date').mockImplementation(() => fixedDate);
const result = getFormattedDate();
expect(result).toBe('2024-01-15');
jest.restoreAllMocks();
});

Stub заменяет функцию фиктивной реализацией.

// Stub через mockImplementation
const fetchUser = jest.fn().mockImplementation(async (id) => {
if (id === 1) return { id: 1, name: 'Alice' };
if (id === 2) return { id: 2, name: 'Bob' };
throw new Error(`User ${id} not found`);
});
test('handles multiple users', async () => {
expect(await fetchUser(1)).toMatchObject({ name: 'Alice' });
expect(await fetchUser(2)).toMatchObject({ name: 'Bob' });
await expect(fetchUser(999)).rejects.toThrow();
});
// Тестирование кода с setTimeout / setInterval / Date.now
test('debounce waits before executing', () => {
jest.useFakeTimers();
const callback = jest.fn();
const debounced = debounce(callback, 300);
debounced(); // запустить
expect(callback).not.toHaveBeenCalled(); // ещё не вызван
jest.advanceTimersByTime(200); // прошло 200мс
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(100); // ещё 100мс = итого 300мс
expect(callback).toHaveBeenCalledTimes(1);
jest.useRealTimers(); // восстановить
});
test('polling stops after max retries', async () => {
jest.useFakeTimers();
const poll = jest.fn()
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null)
.mockResolvedValue({ data: 'ready' });
const result = pollWithRetry(poll, { interval: 1000, maxRetries: 3 });
await jest.runAllTimersAsync();
expect(await result).toEqual({ data: 'ready' });
expect(poll).toHaveBeenCalledTimes(3);
jest.useRealTimers();
});
SpyStubMock
Реальный код✅ работает❌ заменён❌ заменён
Записывает вызовы
Поведение задаётсяНетЯвноЧерез setup
ИспользованиеПроверить вызовФиксировать ответКомплексные сценарии
  1. Создай spy на localStorage.setItem и проверь что данные сохраняются
  2. Используй fake timers для теста retry-логики (5 попыток с интервалом)
  3. Замени реальный axios.get стабом, возвращающим разные данные при разных URL
  • Spy = наблюдатель, реальная функция продолжает работать
  • Stub = полная замена с заданным поведением
  • jest.useFakeTimers() — для тестирования taймаутов и интервалов
  • Всегда вызывай mockRestore() или jest.restoreAllMocks() в afterEach