Async/await в тестах

Jest предоставляет несколько подходов для работы с асинхронными функциями в тестах. Важно понимать, как правильно использовать механизмы синхронизации для тестирования асинхронного кода, чтобы избежать ошибок и обеспечить корректность тестов.

Типы асинхронных функций в JavaScript

Асинхронные операции в JavaScript могут быть реализованы с помощью:

  • Callbacks — функции, которые передаются как аргументы в другие функции и вызываются по завершении работы.
  • Promises — объекты, представляющие результат асинхронной операции, которая может быть завершена с успехом или с ошибкой.
  • async/await — синтаксический сахар для работы с промисами, упрощающий написание асинхронного кода.

Jest поддерживает все эти подходы, но наиболее часто используется синтаксис async/await для работы с асинхронными функциями.

Асинхронные тесты с использованием async/await

В Jest тесты с асинхронными функциями могут быть написаны с применением ключевых слов async и await, что делает код чище и проще для понимания.

Основной синтаксис

Чтобы написать тест с асинхронной функцией, необходимо:

  1. Добавить ключевое слово async перед функцией теста.
  2. Использовать await для ожидания результата асинхронной операции.

Пример:

test('загрузка данных с сервера', async () => {
  const data = await fetchDataFromAPI();
  expect(data).toBeDefined();
});

В данном примере fetchDataFromAPI — это асинхронная функция, которая возвращает промис. Мы используем await, чтобы дождаться завершения этого промиса перед проверкой результата.

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

В случае, если асинхронная операция может завершиться с ошибкой, важно правильно обработать её в тестах. Для этого можно использовать конструкцию try/catch внутри асинхронного теста.

Пример:

test('обработка ошибки при загрузке данных', async () => {
  try {
    await fetchDataFromAPIWithError();
  } catch (error) {
    expect(error).toBeInstanceOf(Error);
  }
});

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

Тестирование асинхронных функций с использованием .resolves и .rejects

Jest предоставляет специальные методы для работы с асинхронными значениями, возвращаемыми промисами: .resolves и .rejects. Эти методы позволяют упростить тесты, избавляя от необходимости явно использовать await.

  • .resolves используется для проверки успешного результата асинхронной операции.
  • .rejects используется для проверки того, что операция завершается с ошибкой.

Пример с .resolves:

test('проверка успешного ответа от API', () => {
  return expect(fetchDataFromAPI()).resolves.toBeDefined();
});

Пример с .rejects:

test('проверка ошибки при загрузке данных', () => {
  return expect(fetchDataFromAPIWithError()).rejects.toThrow('Ошибка');
});

Использование .resolves и .rejects позволяет сделать тесты более лаконичными и выразительными, не прибегая к явному ожиданию результата с помощью await.

Параллельное выполнение асинхронных тестов

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

Очередность выполнения с помощью beforeAll, beforeEach, afterAll, afterEach

Иногда необходимо, чтобы какие-то асинхронные операции выполнялись перед или после тестов. Jest предоставляет хука beforeAll и beforeEach, которые позволяют запускать асинхронные операции перед каждым или перед всеми тестами в группе.

Пример использования beforeAll:

let data;

beforeAll(async () => {
  data = await fetchDataFromAPI();
});

test('проверка данных', () => {
  expect(data).toBeDefined();
});

Здесь beforeAll выполняется перед всеми тестами и загружает данные, которые используются в тестах. Тесты будут выполняться только после того, как асинхронная операция завершится.

Использование done для проверки асинхронных операций

В Jest существует еще один способ работы с асинхронными тестами — это использование функции done. Этот метод обычно используется с обратными вызовами (callbacks), но его можно применять и для асинхронных функций, когда необходимо явно сигнализировать о завершении теста.

Пример с использованием done:

test('проверка асинхронной операции с done', (done) => {
  fetchDataFromAPI()
    .then(data => {
      expect(data).toBeDefined();
      done();
    })
    .catch(done);
});

Этот подход используется реже, поскольку современный синтаксис async/await позволяет писать тесты чище и понятнее.

Тестирование таймеров и задержек

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

Использование jest.useFakeTimers

Если тестируемая функция работает с таймерами (например, setTimeout, setInterval), важно правильно синхронизировать выполнение тестов. Jest позволяет использовать подмену реальных таймеров на фиктивные с помощью метода jest.useFakeTimers.

Пример:

test('проверка работы таймера', () => {
  jest.useFakeTimers();
  
  const mockCallback = jest.fn();
  setTimeout(mockCallback, 1000);
  
  // Прокачиваем время вперед
  jest.advanceTimersByTime(1000);
  
  expect(mockCallback).toHaveBeenCalled();
});

В этом примере используется метод jest.useFakeTimers, который позволяет контролировать таймеры внутри теста и продвигать время с помощью jest.advanceTimersByTime.

Использование async и await с таймерами

Для асинхронных операций, связанных с задержками, можно использовать async/await совместно с методами работы с таймерами.

Пример:

test('проверка асинхронной задержки', async () => {
  await new Promise(resolve => setTimeout(resolve, 1000));
  // проверка после задержки
  expect(true).toBe(true);
});

В данном примере мы использовали Promise с таймером для создания задержки и асинхронной синхронизации.

Советы и лучшие практики

  1. Не смешивайте синхронные и асинхронные операции в одном тесте: Асинхронный код должен быть полностью изолирован от синхронного. Это помогает избежать ошибок и делает тесты более предсказуемыми.
  2. Используйте мок-данные для асинхронных функций: Для упрощения тестов можно замокировать асинхронные операции, такие как HTTP-запросы или обращения к базе данных. Это позволит делать тесты быстрее и не зависеть от внешних сервисов.
  3. Всегда обрабатывайте ошибки: Тесты должны проверять не только успешное выполнение, но и ошибки. Это гарантирует, что ваш код правильно работает в разных условиях.

Асинхронные тесты требуют внимательности, но с правильным подходом они могут стать важной частью эффективного тестирования в JavaScript.