Флюш таймаутов и промисов (flushPromises, jest.useFakeTimers)

В тестировании компонентов Vue часто возникают ситуации, когда нужно корректно обрабатывать асинхронные операции — обновление данных, вызовы API, таймеры и переходы состояний. Для этого в экосистеме Vue Test Utils применяются два ключевых инструмента: flushPromises и таймеры Jest (jest.useFakeTimers). Их использование позволяет создавать стабильные и предсказуемые тесты для асинхронного кода.


flushPromises: завершение всех промисов

flushPromises — это утилита, которая позволяет дождаться выполнения всех ожидающих промисов в текущем цикле событий. В Vue это особенно важно из-за реактивной природы данных: изменения состояния могут инициировать обновление DOM только после завершения всех промисов.

import { mount } from '@vue/test-utils';
import flushPromises from 'flush-promises';
import MyComponent from '@/components/MyComponent.vue';

test('обновление данных после промиса', async () => {
  const wrapper = mount(MyComponent);
  wrapper.vm.fetchData(); // метод, который возвращает промис
  await flushPromises();  // ждём завершения всех промисов
  expect(wrapper.text()).toContain('Данные загружены');
});

Особенности использования flushPromises:

  • Позволяет дождаться всех промисов, включая цепочки then.
  • Не вызывает обновление таймеров, поэтому задержки, реализованные через setTimeout, нужно обрабатывать отдельно.
  • Обычно применяется после вызова методов с асинхронными операциями или при рендеринге, который зависит от данных из API.

Jest fake timers: управление таймерами

Многие компоненты используют setTimeout, setInterval или requestAnimationFrame для отложенного обновления состояния. Для тестирования таких случаев применяется искусственное управление временем через jest.useFakeTimers.

jest.useFakeTimers();

test('ожидание setTimeout', () => {
  const callback = jest.fn();
  setTimeout(callback, 1000);

  jest.advanceTimersByTime(1000); // перемещаем виртуальные часы на 1 секунду
  expect(callback).toHaveBeenCalled();
});

Ключевые моменты при работе с fake timers в Vue:

  • Сочетание с flushPromises: если внутри таймера вызывается асинхронный код, необходимо сначала продвинуть таймеры, а затем выполнить flushPromises.
  • jest.runAllTimers() vs jest.advanceTimersByTime(ms): первый метод выполняет все запланированные таймеры сразу, второй позволяет контролировать временной промежуток.
  • Обновление компонента после таймера: после изменения состояния внутри таймера требуется await wrapper.vm.$nextTick(), чтобы Vue успел применить реактивные изменения к DOM.
test('асинхронное обновление с таймером', async () => {
  const wrapper = mount(MyComponent);
  wrapper.vm.startTimer(); // метод с setTimeout и асинхронной операцией

  jest.advanceTimersByTime(500);  // промежуточное время
  expect(wrapper.text()).not.toContain('Готово');

  jest.advanceTimersByTime(500);  // таймер завершен
  await flushPromises();           // ждём завершения промиса
  await wrapper.vm.$nextTick();    // ждём обновления DOM
  expect(wrapper.text()).toContain('Готово');
});

Комбинирование flushPromises и fake timers

На практике часто встречаются сценарии, где компонент одновременно использует асинхронные промисы и таймеры. Для корректного тестирования важно соблюдать последовательность действий:

  1. Активировать fake timers (jest.useFakeTimers()).
  2. Инициировать событие или метод, который запускает таймер и промис.
  3. Продвинуть таймеры с помощью jest.advanceTimersByTime(ms) или jest.runAllTimers().
  4. Вызвать await flushPromises() для завершения всех промисов.
  5. Дождаться обновления DOM через await wrapper.vm.$nextTick().

Эта последовательность гарантирует, что все асинхронные операции полностью завершены перед проверкой состояния компонента.


Практические рекомендации

  • Не смешивать реальные и фейковые таймеры в одном тесте — это приводит к непредсказуемым результатам.
  • Всегда использовать await flushPromises() после промисов, даже если таймер уже завершился, иначе изменения состояния могут быть не применены.
  • Фокус на реактивность: после любого изменения состояния компонента рекомендуется дожидаться следующего тика Vue ($nextTick) для актуального DOM.
  • Модульность тестов: разделять тесты на «чисто асинхронные» и «чисто таймерные» позволяет легче локализовать ошибки.

Инструменты и зависимости

Для полноценного использования этих техник в проекте необходимы:

  • @vue/test-utils — основной инструмент для рендеринга и взаимодействия с компонентами.
  • flush-promises — библиотека для ожидания всех промисов.
  • jest — тестовый рантайм с поддержкой fake timers и моков.

Настройка fake timers выполняется на уровне теста или в beforeEach/beforeAll:

beforeEach(() => {
  jest.useFakeTimers();
});

afterEach(() => {
  jest.runOnlyPendingTimers();
  jest.useRealTimers();
});

Такой подход обеспечивает чистое состояние между тестами и предотвращает утечки таймеров.


Использование комбинации flushPromises и fake timers обеспечивает полный контроль над асинхронными процессами и таймерами в тестах Vue-компонентов, делая тесты надежными, предсказуемыми и независимыми от внешних задержек.