Асинхронные тесты
Большинство реального кода асинхронно: 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;
});
Практика
- Напиши async-тест для функции
fetchPosts(userId)с моком fetch - Протестируй callback-функцию
readFile(path, callback)через done - Используй fake timers для тестирования
setInterval-based polling - Протестируй retry-логику: функция пробует 3 раза с задержкой и возвращает результат или ошибку