9. Spies и Stubs

Spy: наблюдать за реальной функцией
Заголовок раздела «Spy: наблюдать за реальной функцией»Spy оборачивает реальную функцию, позволяя следить за вызовами, но сохраняя оригинальное поведение.
import { jest } from '@jest/globals'; // или vi from 'vitest'
// Базовый spyconst 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(); // восстановить оригиналПрактический пример: spy на методе класса
Заголовок раздела «Практический пример: spy на методе класса»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 logSpy = jest.spyOn(logger, 'log');
const service = new UserService(db, logger);
expect(logSpy).toHaveBeenCalledTimes(2); expect(logSpy).toHaveBeenNthCalledWith(2, expect.stringContaining('User created'));
logSpy.mockRestore();});Spy на встроенных объектах
Заголовок раздела «Spy на встроенных объектах»// Spy на consoletest('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 на Datetest('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: полная замена»Stub заменяет функцию фиктивной реализацией.
// Stub через mockImplementationconst 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();});Fake Timers
Заголовок раздела «Fake Timers»// Тестирование кода с 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();});Разница Spy, Stub, Mock
Заголовок раздела «Разница Spy, Stub, Mock»| Spy | Stub | Mock | |
|---|---|---|---|
| Реальный код | ✅ работает | ❌ заменён | ❌ заменён |
| Записывает вызовы | ✅ | ✅ | ✅ |
| Поведение задаётся | Нет | Явно | Через setup |
| Использование | Проверить вызов | Фиксировать ответ | Комплексные сценарии |
Практические задания
Заголовок раздела «Практические задания»- Создай spy на
localStorage.setItemи проверь что данные сохраняются - Используй fake timers для теста retry-логики (5 попыток с интервалом)
- Замени реальный
axios.getстабом, возвращающим разные данные при разных URL
- Spy = наблюдатель, реальная функция продолжает работать
- Stub = полная замена с заданным поведением
- jest.useFakeTimers() — для тестирования taймаутов и интервалов
- Всегда вызывай mockRestore() или jest.restoreAllMocks() в afterEach