Тестирование асинхронного кода: async/await, done(), fake timers

Почему асинхронность — это вызов для тестирования? 🔥

Асинхронный код в JavaScript ведёт себя как непослушный щенок — его выполнение непредсказуемо! Без правильного подхода тесты могут проходить "случайно" или ломаться из-за таймеров. Разберём три ключевых инструмента для укрощения асинхронности:

  1. async/await — элегантный синтаксис
  2. done() — классический колбэк-подход
  3. Fake Timers — контроль времени

Async/Await: Тестирование как синхронный код ✨

Современный стандарт, превращающий асинхронные тесты в понятные последовательные инструкции.

test('fetchData возвращает правильные данные', async () => {
  const data = await fetchData(); // Ждём результат
  expect(data).toEqual({ id: 1, name: 'Danila' });
});

Где ловушки?

  • Не забывайте async перед функцией теста
    Без него await вызовет синтаксическую ошибку.

  • Ошибки не будут отловлены без дополнительного try/catch:

test('обработка ошибок при fetchData', async () => {
  await expect(fetchData('invalid-url')).rejects.toThrow('404');
});

💡 Pro Tip: Используйте .resolves/.rejects из Jest для более чистого кода!


Done(): Старая школа, но с характером 🎸

Классический подход для тестирования кода с колбэками. Передаём функцию done и вызываем её вручную.

test('setTimeout вызывает колбэк', (done) => {
  function callback(data) {
    expect(data).toBe('Поехали!');
    done(); // Говорим Jest: "Тест завершён!"
  }

  asyncFunctionWithCallback(callback);
});

Опасные моменты:

  • Забыли вызвать done()? Тест зависнет и упадёт по таймауту.
  • Вызвали done() дважды? Получите ошибку "Done was called multiple times".
  • Необработанная ошибка? Добавьте try/catch внутри колбэка.
test('ошибка в колбэке', (done) => {
  function callback(err, data) {
    if (err) {
      done(err); // Передаём ошибку в Jest
      return;
    }
    // Проверки данных...
    done();
  }

  riskyAsyncOperation(callback);
});

Fake Timers: Магия контроля времени ⏱️

Как тестировать setTimeout или setInterval без реального ожидания? Fake Timers подменяют системные таймеры!

jest.useFakeTimers(); // Активируем фейковые таймеры

test('таймер срабатывает через 1 секунду', () => {
  const callback = jest.fn();

  setTimeout(callback, 1000);
  expect(callback).not.toHaveBeenCalled(); // Пока не вызван

  jest.runAllTimers(); // Прокручиваем ВСЕ таймеры мгновенно
  expect(callback).toHaveBeenCalled();
});

Продвинутые техники:

  • jest.runOnlyPendingTimers() — только активные таймеры
  • jest.advanceTimersByTime(ms) — перемотка на N миллисекунд
  • Очистка после теста:
afterEach(() => {
  jest.useRealTimers(); // Возвращаем настоящие таймеры
});

🚨 Важно! Fake Timers не работают с promises "из коробки" — для них нужны дополнительные плагины.


Выбираем инструмент под задачу 🧰

Ситуация Инструмент Когда использовать?
Промисы/асинхронные функции async/await Почти всегда — это стандарт!
Колбэки done() Легаси-код или stream-based API
Таймеры/интервалы Fake Timers Тесты, зависящие от времени

Реальный пример: Тестируем «умный повторитель» 🔄

Разберём кейс: функция retryAsyncRequest делает запрос и повторяет его при ошибке с интервалом.

async function retryAsyncRequest(requestFn, retries = 3, delay = 1000) {
  try {
    return await requestFn();
  } catch (error) {
    if (retries <= 1) throw error;
    await new Promise(res => setTimeout(res, delay));
    return retryAsyncRequest(requestFn, retries - 1, delay);
  }
}

Пишем тест с комбинацией техник:

jest.useFakeTimers();

test('retryAsyncRequest повторяет запрос 3 раза', async () => {
  const failingRequest = jest.fn()
    .mockRejectedValueOnce(new Error('Timeout'))
    .mockRejectedValueOnce(new Error('Timeout'))
    .mockResolvedValue({ data: 'Успех!' });

  const promise = retryAsyncRequest(failingRequest);

  jest.advanceTimersByTime(2000); // Пропускаем 2 секунды
  const result = await promise;

  expect(failingRequest).toHaveBeenCalledTimes(3);
  expect(result).toEqual({ data: 'Успех!' });
});

Главные правила тестирования асинхронности 🏆

  1. Всегда проверяйте количество вызовов — асинхронный код может выполниться неожиданное число раз.
  2. Контролируйте побочные эффекты — очищайте таймеры, моки и состояния между тестами.
  3. Тестируйте ошибки — сбойные сценарии важнее «счастливого пути».
  4. Избегайте sleep() в тестах — Fake Timers ускорят их в сотни раз.

Помните: хорошо протестированный асинхронный код — это стабильность вашего приложения. Когда в следующий раз увидите setTimeout в коде — вспомните про jest.advanceTimersByTime() и улыбнитесь! 😎

Скрыть рекламу навсегда

🌱 Индвидидулаьные занятия

Индивидуальные онлайн-занятия по программированию для детей и подростков

Личный подход, без воды, с фокусом на понимание и реальные проекты.

🚀 Записаться на занятие