Обработка ошибок в асинхронных операциях

В тестировании компонентов Vue с помощью Vue Test Utils критически важно корректно работать с асинхронными операциями, поскольку современные приложения часто взаимодействуют с API, используют таймеры или промисы. Ошибки, возникающие в этих операциях, могут приводить к непредсказуемым результатам тестов, поэтому необходимо научиться их правильно перехватывать и проверять.


Асинхронные методы и ошибки

Компоненты Vue часто содержат методы, возвращающие промисы:

export default {
  methods: {
    async fetchData() {
      const response = await fetch('/api/data');
      if (!response.ok) throw new Error('Ошибка при получении данных');
      return await response.json();
    }
  }
}

При тестировании таких методов нужно учитывать, что ошибка внутри промиса не выбрасывается немедленно, а лишь отклоняет промис. Попытка вызвать метод без использования await или .catch приведет к неперехваченной ошибке и падению теста.


Использование async/await в тестах

Для перехвата ошибок асинхронных методов применяется try/catch внутри асинхронной функции теста:

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

test('метод fetchData выбрасывает ошибку при неправильном ответе', async () => {
  const wrapper = mount(MyComponent);
  
  global.fetch = jest.fn(() =>
    Promise.resolve({ ok: false })
  );

  try {
    await wrapper.vm.fetchData();
  } catch (e) {
    expect(e).toBeInstanceOf(Error);
    expect(e.message).toBe('Ошибка при получении данных');
  }
});

Ключевые моменты:

  • Асинхронный метод тестируется через await.
  • Ошибки перехватываются в try/catch.
  • Используется проверка типа ошибки и сообщения для уверенности в корректной обработке.

Использование Jest .rejects

Jest предоставляет удобный способ тестирования промисов с ожидаемым отклонением через .rejects:

test('fetchData возвращает ошибку', async () => {
  const wrapper = mount(MyComponent);

  global.fetch = jest.fn(() =>
    Promise.resolve({ ok: false })
  );

  await expect(wrapper.vm.fetchData()).rejects.toThrow('Ошибка при получении данных');
});

Преимущества подхода .rejects:

  • Код теста короче.
  • Нет необходимости писать try/catch.
  • Явно выражает ожидание, что промис завершится с ошибкой.

Ошибки при обновлении DOM после асинхронных операций

Vue обновляет DOM асинхронно. После вызова асинхронного метода компонента необходимо дождаться следующего «типа цикла реактивности», чтобы проверить состояние компонента:

import { nextTick } from 'vue';

test('ошибка отображается в компоненте', async () => {
  const wrapper = mount(MyComponent);

  wrapper.vm.fetchData = jest.fn(() => Promise.reject(new Error('Ошибка')));

  await wrapper.vm.fetchData().catch(() => {});
  await nextTick();

  expect(wrapper.text()).toContain('Произошла ошибка');
});

Особенности:

  • nextTick используется для ожидания обновления DOM после изменения состояния.
  • catch необходим, чтобы тест не падал из-за необработанного промиса.
  • Текст ошибки проверяется через wrapper.text() или другие селекторы.

Моки асинхронных зависимостей

Для контроля ошибок рекомендуется мокировать внешние зависимости, например, API-запросы:

jest.mock('@/api', () => ({
  getData: jest.fn()
}));

import { getData } from '@/api';
import MyComponent from '@/components/MyComponent.vue';
import { mount } from '@vue/test-utils';

test('fetchData правильно обрабатывает ошибку API', async () => {
  getData.mockRejectedValue(new Error('Ошибка API'));

  const wrapper = mount(MyComponent);

  await wrapper.vm.fetchData().catch(() => {});
  await nextTick();

  expect(wrapper.text()).toContain('Произошла ошибка');
});

Преимущества моков:

  • Полный контроль над асинхронным поведением.
  • Возможность проверять разные сценарии ошибок.
  • Изоляция тестируемого компонента от реального API.

Комбинирование таймеров и асинхронных ошибок

Если метод использует setTimeout или другие таймеры, нужно использовать Jest Fake Timers:

jest.useFakeTimers();

test('ошибка через таймер', async () => {
  const wrapper = mount(MyComponent);

  wrapper.vm.delayedError = () =>
    new Promise((_, reject) => setTimeout(() => reject(new Error('Ошибка таймера')), 1000));

  const promise = wrapper.vm.delayedError();

  jest.advanceTimersByTime(1000);

  await expect(promise).rejects.toThrow('Ошибка таймера');
});

Особенности:

  • jest.useFakeTimers() позволяет контролировать таймеры.
  • jest.advanceTimersByTime ускоряет время ожидания.
  • Ошибка тестируется как у обычного промиса.

Выводы по обработке ошибок

  • Асинхронные методы требуют использования await или .then/.catch.
  • Проверка ошибок через .rejects делает код лаконичным.
  • Для тестирования состояния компонента после ошибки необходимо использовать nextTick.
  • Моки внешних зависимостей и управление таймерами обеспечивают предсказуемость тестов.
  • Явное перехватывание ошибок предотвращает падение тестов из-за необработанных промисов.

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