Асинхронные тесты

Большинство реального кода асинхронно: API-запросы, чтение файлов, таймеры. Jest предлагает несколько способов тестирования асинхронного кода.

Зачем нужно

Если не обработать асинхронность правильно, тест завершится ДО выполнения проверки и всегда будет зелёным — даже если код сломан. Jest должен знать, когда асинхронная операция закончилась.

Где используется

Тесты API-вызовов, работа с БД, файловые операции, таймеры, промисы, EventEmitter.

Предпосылки

Настройка Jest, Matchers, промисы и async/await в JavaScript

Способ 1: async/await (рекомендуемый)

// fetchUser.js
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error('Пользователь не найден');
  return response.json();
}

// fetchUser.test.js
test('загружает пользователя по id', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Алиса');
});

test('выбрасывает ошибку для несуществующего пользователя', async () => {
  await expect(fetchUser(999)).rejects.toThrow('не найден');
});

Важно: не забудь await!

// ПЛОХО: тест пройдёт даже если промис отклонён
test('БЕЗ await — тест всегда зелёный!', () => {
  // Нет await — Jest не ждёт результат
  expect(fetchUser(1)).resolves.toBeDefined(); // Промис просто создаётся
});

// ХОРОШО: await заставляет Jest ждать
test('С await — корректная проверка', async () => {
  await expect(fetchUser(1)).resolves.toBeDefined();
});

Способ 2: return Promise

test('загружает данные (return promise)', () => {
  // Jest ждёт, пока промис разрешится
  return fetchUser(1).then(user => {
    expect(user.name).toBe('Алиса');
  });
});

// Для ошибок
test('ошибка при невалидном id (return promise)', () => {
  return fetchUser(999).catch(error => {
    expect(error.message).toBe('Пользователь не найден');
  });
});

Способ 3: resolves / rejects

test('resolves — промис успешен', async () => {
  await expect(fetchUser(1)).resolves.toEqual({
    id: 1,
    name: 'Алиса',
  });
});

test('rejects — промис отклонён', async () => {
  await expect(fetchUser(999)).rejects.toThrow('не найден');
  await expect(fetchUser(999)).rejects.toBeInstanceOf(Error);
});

Способ 4: done callback (устаревший)

// Для callback-based кода
function loadData(callback) {
  setTimeout(() => {
    callback(null, { data: 42 });
  }, 100);
}

test('загружает данные через callback', (done) => {
  loadData((err, result) => {
    try {
      expect(err).toBeNull();
      expect(result.data).toBe(42);
      done; // Сигнал: тест завершён
    } catch (error) {
      done(error); // Передаём ошибку в Jest
    }
  });
});

Ловушка done без try/catch

// ПЛОХО: если expect упадёт, done не вызовется → таймаут
test('без try/catch', (done) => {
  loadData((err, result) => {
    expect(result.data).toBe(99); // Падает, done не вызван
    done; // Никогда не выполнится
  });
});
// Результат: таймаут через 5 секунд, неинформативная ошибка

Fake Timers — управление временем

// delayedGreeting.js
function delayedGreeting(name, callback) {
  setTimeout(() => {
    callback(`Привет, ${name}!`);
  }, 3000);
}

// delayedGreeting.test.js
test('вызывает callback через 3 секунды', () => {
  jest.useFakeTimers; // Включаем фейковые таймеры

  const callback = jest.fn;
  delayedGreeting('Алиса', callback);

  // Callback ещё не вызван
  expect(callback).not.toHaveBeenCalled();

  // Перематываем время на 3 секунды
  jest.advanceTimersByTime(3000);

  // Теперь вызван
  expect(callback).toHaveBeenCalledWith('Привет, Алиса!');

  jest.useRealTimers; // Восстанавливаем реальные таймеры
});

Методы управления таймерами

jest.useFakeTimers;         // Включить фейковые таймеры
jest.useRealTimers;         // Вернуть реальные

jest.advanceTimersByTime(ms); // Перемотать на N мс
jest.runAllTimers;          // Выполнить ВСЕ таймеры
jest.runOnlyPendingTimers;  // Только текущие (без вложенных)

jest.setSystemTime(date);     // Установить конкретную дату
jest.getRealSystemTime;     // Получить реальное время

Пример: debounce

function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout( => fn(...args), ms);
  };
}

test('debounce вызывает функцию после паузы', () => {
  jest.useFakeTimers;
  const fn = jest.fn;
  const debounced = debounce(fn, 500);

  debounced('a');
  debounced('b');
  debounced('c'); // Только этот вызов должен пройти

  jest.advanceTimersByTime(500);

  expect(fn).toHaveBeenCalledTimes(1);
  expect(fn).toHaveBeenCalledWith('c');

  jest.useRealTimers;
});

Пример: Date.now()

test('генерирует токен с текущей датой', () => {
  jest.useFakeTimers;
  jest.setSystemTime(new Date('2025-06-15T12:00:00Z'));

  const token = generateToken('user123');

  expect(token).toContain('2025-06-15');
  expect(token).toContain('user123');

  jest.useRealTimers;
});

Таймаут для тестов

// Для одного теста
test('долгая операция', async () => {
  await longRunningTask;
}, 15000); // 15 секунд вместо 5 по умолчанию

// Для всех тестов в файле
jest.setTimeout(10000);

// В jest.config.js для всего проекта
module.exports = {
  testTimeout: 10000,
};

Параллельные промисы

test('загружает несколько ресурсов параллельно', async () => {
  const [users, posts, comments] = await Promise.all([
    fetchUsers,
    fetchPosts,
    fetchComments,
  ]);

  expect(users).toHaveLength(5);
  expect(posts).toHaveLength(10);
  expect(comments).toHaveLength(20);
});

test('Promise.allSettled — все результаты', async () => {
  const results = await Promise.allSettled([
    fetchUser(1),     // fulfilled
    fetchUser(999),   // rejected
  ]);

  expect(results[0].status).toBe('fulfilled');
  expect(results[1].status).toBe('rejected');
});

Частые ошибки

1. Забыли async/await

// ПЛОХО: тест всегда зелёный
test('проверка без await', () => {
  expect(fetchData).resolves.toBe(42); // Нет await!
});

// ХОРОШО:
test('проверка с await', async () => {
  await expect(fetchData).resolves.toBe(42);
});

2. Фейковые таймеры + реальные промисы

// ПРОБЛЕМА: Promise разрешается в микрозадаче,
// но jest.advanceTimersByTime не обрабатывает микрозадачи

// Решение: дать промису разрешиться
test('таймер внутри async функции', async () => {
  jest.useFakeTimers;

  const promise = delayedFetch;

  jest.advanceTimersByTime(1000);
  await Promise.resolve; // Даём микрозадачам отработать

  const result = await promise;
  expect(result).toBeDefined();

  jest.useRealTimers;
});

3. Не очистили таймеры

// ХОРОШО: очищаем после каждого теста
afterEach(() => {
  jest.useRealTimers;
});

Практика

  1. Напиши async-тест для функции fetchPosts(userId) с моком fetch
  2. Протестируй callback-функцию readFile(path, callback) через done
  3. Используй fake timers для тестирования setInterval-based polling
  4. Протестируй retry-логику: функция пробует 3 раза с задержкой и возвращает результат или ошибку

Связанные темы

Ресурсы