Тестирование Promise.all и Promise.race

Promise.all и Promise.race — это два метода работы с асинхронными операциями в JavaScript, которые часто используются для выполнения нескольких асинхронных задач параллельно. Несмотря на свою популярность, тестирование этих методов требует некоторых специфических подходов. В этой статье рассматриваются основные техники и стратегии тестирования этих методов с использованием Jest.

Promise.all

Метод Promise.all принимает массив или иной итерируемый объект, содержащий несколько промисов, и возвращает новый промис, который:

  • Исполнится, когда все промисы внутри массива будут исполнены.
  • Отменится (с ошибкой), если хотя бы один из промисов завершится с ошибкой.

При тестировании Promise.all важно учитывать как успешные сценарии, так и те, где один из промисов отклоняется.

Пример теста для успешного выполнения всех промисов:
test('Promise.all выполняет все промисы успешно', async () => {
  const promise1 = Promise.resolve(3);
  const promise2 = Promise.resolve(42);
  const promise3 = Promise.resolve('Hello');

  await expect(Promise.all([promise1, promise2, promise3])).resolves.toEqual([3, 42, 'Hello']);
});

В данном примере создаются три промиса, которые успешно разрешаются. Мы используем Jest-метод resolves для проверки того, что результат выполнения Promise.all соответствует ожидаемому массиву значений.

Пример теста для отклонения хотя бы одного промиса:
test('Promise.all отклоняется при отклонении одного промиса', async () => {
  const promise1 = Promise.resolve(3);
  const promise2 = Promise.reject(new Error('Failed'));
  const promise3 = Promise.resolve('Hello');

  await expect(Promise.all([promise1, promise2, promise3])).rejects.toThrow('Failed');
});

Здесь мы проверяем, что если один из промисов отклоняется, Promise.all также отклонится. Метод rejects позволяет тестировать отклонения промисов, и в данном случае мы ожидаем ошибку с сообщением ‘Failed’.

Promise.race

Метод Promise.race работает немного иначе. Он принимает массив промисов и возвращает новый промис, который:

  • Сразу же выполняется или отклоняется по завершению первого промиса из массива, независимо от состояния остальных.

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

Пример теста для успешного завершения первого промиса:
test('Promise.race выполняется с первым успешным промисом', async () => {
  const promise1 = new Promise(resolve => setTimeout(resolve, 100, 'First'));
  const promise2 = new Promise(resolve => setTimeout(resolve, 200, 'Second'));
  
  await expect(Promise.race([promise1, promise2])).resolves.toBe('First');
});

В этом тесте Promise.race должен завершиться первым успешным промисом, даже если второй промис разрешится позже. Тест подтверждает, что первый результат ‘First’ будет возвращен в результате выполнения.

Пример теста для отклонения первого промиса:
test('Promise.race отклоняется с первым отклонённым промисом', async () => {
  const promise1 = new Promise((_, reject) => setTimeout(reject, 100, 'Error 1'));
  const promise2 = new Promise((_, reject) => setTimeout(reject, 200, 'Error 2'));
  
  await expect(Promise.race([promise1, promise2])).rejects.toBe('Error 1');
});

Здесь Promise.race отклоняется с тем промисом, который завершится первым. В этом случае это будет отклонение с сообщением ‘Error 1’. Такой тест помогает удостовериться, что метод правильно обрабатывает сценарии с ошибками.

Тестирование с использованием mock-функций и таймера

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

Пример использования mock-функций для асинхронных операций:
test('Promise.all с мок-данными', async () => {
  const mock1 = jest.fn(() => Promise.resolve('Mocked 1'));
  const mock2 = jest.fn(() => Promise.resolve('Mocked 2'));
  const mock3 = jest.fn(() => Promise.resolve('Mocked 3'));

  const result = await Promise.all([mock1(), mock2(), mock3()]);
  expect(result).toEqual(['Mocked 1', 'Mocked 2', 'Mocked 3']);
});

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

Пример с использованием таймеров:
jest.useFakeTimers();

test('Promise.race с задержками', async () => {
  const promise1 = new Promise(resolve => setTimeout(resolve, 100, 'First'));
  const promise2 = new Promise(resolve => setTimeout(resolve, 50, 'Second'));

  const racePromise = Promise.race([promise1, promise2]);

  jest.runAllTimers();

  await expect(racePromise).resolves.toBe('Second');
});

В этом примере используется Jest для имитации таймеров. С помощью jest.useFakeTimers() можно точно контролировать задержки, чтобы убедиться, что Promise.race завершится с самым быстрым промисом.

Особенности тестирования с использованием различных конфигураций Jest

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

Однако при использовании методов .then или .catch важно явно завершать тест с помощью done(), если это необходимо, чтобы Jest знал, когда завершился асинхронный процесс.

Пример с использованием done() для завершения теста:
test('Promise.all с использованием done', done => {
  const promise1 = Promise.resolve(1);
  const promise2 = Promise.resolve(2);

  Promise.all([promise1, promise2]).then(result => {
    expect(result).toEqual([1, 2]);
    done();
  });
});

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

Важные замечания

  1. Порядок выполнения промисов: Тестирование с Promise.all и Promise.race часто связано с параллельным выполнением, что означает, что порядок завершения промисов может быть важен.

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

  3. Тестирование с задержками: Иногда важно тестировать промисы с имитацией задержек, чтобы проверить правильность работы метода с реальными временными задержками и таймерами.

Таким образом, тестирование асинхронных операций с использованием Promise.all и Promise.race в Jest требует внимательности к таким аспектам, как порядок выполнения, ошибки и тайминги. Применение Jest-методов, таких как resolves, rejects, а также мок-функций и фейковые таймеры, позволяет создавать точные и надежные тесты для асинхронных операций в JavaScript.