Тестирование асинхронного кода: async/await, done(), fake timers
Почему асинхронность — это вызов для тестирования? 🔥
Асинхронный код в JavaScript ведёт себя как непослушный щенок — его выполнение непредсказуемо! Без правильного подхода тесты могут проходить "случайно" или ломаться из-за таймеров. Разберём три ключевых инструмента для укрощения асинхронности:
async/await— элегантный синтаксисdone()— классический колбэк-подход- 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: 'Успех!' });
});
Главные правила тестирования асинхронности 🏆
- Всегда проверяйте количество вызовов — асинхронный код может выполниться неожиданное число раз.
- Контролируйте побочные эффекты — очищайте таймеры, моки и состояния между тестами.
- Тестируйте ошибки — сбойные сценарии важнее «счастливого пути».
- Избегайте sleep() в тестах — Fake Timers ускорят их в сотни раз.
Помните: хорошо протестированный асинхронный код — это стабильность вашего приложения. Когда в следующий раз увидите setTimeout в коде — вспомните про jest.advanceTimersByTime() и улыбнитесь! 😎