Асинхронный тест обязан вернуть или дождаться свою работу, иначе Vitest завершит его раньше, чем промис разрешится. Если ассерт спрятан внутри .then без return и без await, тест закончится до того, как колбэк выполнится, - и проверка просто не запустится. Формально тест зелёный, фактически он не проверил ничего. Это самая частая причина ложного 'прошло' в асинхронном коде.
Правильная форма - вернуть промис или дождаться его через await. Vitest понимает возвращённый промис и ждёт его завершения, а матчеры .resolves и .rejects делают ожидание явным. Сравните две записи одного теста: в хрупкой ассерт висит в .then, в надёжной результат ожидается через await expect(load()).resolves. Вторая гарантирует, что проверка действительно состоялась.
Для ошибок есть .rejects: await expect(loadUser('missing')).rejects.toThrow('User not found'). Обязательно await - без него отклонённый промис снова проскочит мимо. Чтобы полностью закрыть риск незапущенной проверки, добавляют expect.assertions(1): тест упадёт, если внутри не выполнилось ровно одно утверждение, - надёжная страховка для асинхронных путей с ветвлением.
Забытый return или await - источник ложного 'прошло' номер один. Тест зелёный, потому что до ассерта дело не дошло, а не потому, что поведение верное. Такой тест не ловит регресс: сломайте код - он всё равно останется зелёным. Поэтому в асинхронных тестах взгляд первым делом ищет, дожидается ли тест каждой асинхронной операции, результат которой он проверяет. Практический приём - у каждого expect внутри асинхронного колбэка спросить себя, дождался ли тест этот колбэк, и если нет, вернуть или await его.
Фиксированную задержку через sleep использовать не стоит. Она одновременно замедляет suite и не гарантирует отсутствие гонки: на быстрой машине пройдёт, на нагруженном CI - нет. Ждите наблюдаемый результат, а не время: resolves/rejects для промиса, findBy и waitFor для UI, явное условие для фоновой работы. Ожидание по результату детерминировано, ожидание по таймеру - нет.
Когда в тесте несколько промисов, их синхронизируют явно: await Promise.all для параллельных или последовательный await, если важен порядок. Полагаться на то, что операции 'успеют' сами, нельзя - это та же гонка, только спрятанная. Явная синхронизация делает намерение видимым и убирает зависимость от скорости окружения.
Устаревший колбэк done лучше не использовать. Он глотает ошибки внутри асинхронных колбэков и вместо ясного падения даёт таймаут, по которому трудно понять причину. Современная форма - async-функция с await; она короче, читается линейно и корректно пробрасывает ошибки в отчёт. done оставляют лишь редким событийным API, где иначе никак.
Типичный провал - позднее unhandled rejection: промис отклонился, но его никто не дождался, и ошибка всплывает в отчёте другого теста или после завершения suite. Диагностировать такое тяжело, потому что место падения не совпадает с местом причины. Дисциплина 'вернуть или await' плюс expect.assertions закрывают этот класс: каждая асинхронная работа дожидается там, где начата.
// Хрупко: ассерт в .then, тест завершится раньше.
test('loads', () => {
load().then((data) => {
expect(data.ok).toBe(true)
})
})
// Надёжно: Vitest ждёт промис.
test('loads', async () => {
await expect(load()).resolves.toMatchObject({ ok: true })
})test('отклоняет неизвестного пользователя', async () => {
expect.assertions(1)
await expect(loadUser('missing')).rejects.toThrow('User not found')
})