Использование forEach и async/await ведет себя по-разному для узла и Jest

У меня есть функция, которая записывает данные в mongodb, например:

const writeToDB = async (db, data) => {
  const dataKeys = Object.keys(data)
  dataKeys.forEach(async key => db.collection(key).insertMany(data[key]))
}

Это отлично работает, если я запускаю его в сценарии узла. Но когда я попытался использовать его в Jest beforeAll, я получил эту асинхронную ошибку от Jest:

Jest did not exit one second after the test run has completed. This usually means that there are asynchronous operations that weren't stopped in your tests.

после некоторого устранения неполадок я обнаружил, что forEach вызывает проблемы. Использование цикла for решило эту проблему:

const writeToDB = async (db, data) => {
  const dataKeys = Object.keys(data)
  for (const key of dataKeys) {
    await db.collection(key).insertMany(data[key])
  }
}

В поисках этой проблемы я наткнулся на эту статью: https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404

Объяснение имело смысл, но оставило у меня несколько вопросов:

  • По статье не должно было работать даже в узле сценарий. Как получилось?
  • Почему его выполнение в узле отличается от запуска в Jest?

редактировать

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

forEach не await для async обратного вызова для урегулирования.
nicholaswmin 18.02.2019 17:07

Jest жалуется, потому что процесс все еще выполняется после завершения теста — процесс не может завершиться, пока все промисы не будут выполнены. Node не возражает, потому что такой проверки нет, он просто поддерживает процесс. Вы также можете написать await Promise.all(dataKeys.map((key) => db.collection(key).insertMany(data[key]));, что позволяет выполнять параллельные операции.

jonrsharpe 18.02.2019 17:08

@jonrsharpe, но почему процесс завершился правильно, когда я запустил его в обычном сценарии узла?

devboell 18.02.2019 17:10

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

jonrsharpe 18.02.2019 17:11

Почему обратный вызов forEach асинхронный, если вы не используете Ждите? Вы что-то упускаете здесь.

MadWard 18.02.2019 17:12

@MadWard, как я понял из правила eslint без возврата, ожидание в этом случае не требуется

devboell 18.02.2019 17:20

Что именно вы подразумеваете под «Это отлично работает, если я запускаю его в сценарии узла.». Как это работает "хорошо", что вы тестировали? Вы удивлены, что он запускал все прошивки?

Bergi 18.02.2019 17:35
По статье не должно было работать даже в нодовом скрипте - зависит от скрипта. Почему его выполнение в узле отличается от запуска в Jest? - нет никаких доказательств того, что он был выполнен по-другому. Предупреждения нет, потому что это предупреждение Джеста.
Estus Flask 18.02.2019 17:47

Возможный дубликат stackoverflow.com/questions/37576685/…

Kevin B 18.02.2019 17:55
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
9
4 072
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Асинхронные функции работают даже в контекстах, которые не вызывают их await или не вызывают .then(), то есть я точно могу это сделать:

async function foo() {
  // async calls here
}

foo(); // no await or .then().

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

Основное отличие состоит в том, что .forEach() не заботится о завершении операций или ожидает их завершения перед вызовом следующей (поскольку асинхронные функции return выполняются немедленно), тогда как ваш вызов for..of использует await для ожидания завершения каждой операции, прежде чем перейти к следующей. .

Ваш первый пример .forEach был бы примерно эквивалентен нижнему, если бы вы удалили await из вызова внутри цикла.

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


В этой заметке два примера не эквивалентны, потому что первый будет вызывать insertMany один за другим, не дожидаясь их завершения, что приводит к параллельному выполнению вызовов БД.

Если вы хотите сохранить это поведение, но по-прежнему возвращать правильное обещание, которое ожидает завершения всего, вам следует использовать [].map() вместо [].forEach:

const writeToDB = async (db, data) => {
  const dataKeys = Object.keys(data)
  const allPromises = dataKeys.map(async key =>
    await db.collection(key).insertMany(data[key])) // run all calls in parallel
  return await Promise.all(allPromises); // wait for them all to finish
}

Думаю, вы ответили на мой первый вопрос. Дело не в том, что node выдаст ошибку, если forEach не ждет, ему просто все равно, и он выполняется параллельно, как вы упомянули. Но почему шутка волнует? как он обнаруживает, что обещания не были ожидаемы?

devboell 18.02.2019 17:26

Спасибо, я принял ответ Эстуса, потому что он ответил на второй вопрос. Но это моя ошибка, я не должен был задавать два вопроса сразу.

devboell 18.02.2019 18:54

@devboell Ну, Jest - это средство запуска тестов, оно предназначено для поиска этих вещей, потому что они указывают на ошибки :)

Madara's Ghost 18.02.2019 23:10
Ответ принят как подходящий

Существующий ответ уже подробно объясняет, почему forEach не следует использовать с обещаниями так, как он используется. forEach обратный вызов не учитывает возвращенные обещания и разрывает цепочку обещаний. async..await необходимо использовать с for..of для последовательной оценки обещаний или с Promise.all и map для параллельной оценки.

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

Как только Jest завершает тест, он проверяет, есть ли открытые дескрипторы, препятствующие выходу Node. Поскольку обещания не возвращались и не связывались Jest, процессы, которые они представляли, не позволяли Jest завершить процесс тестирования.

Эта проблема представлена ​​указанным сообщением об ошибке:

Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles to troubleshoot this issue.

Другие вопросы по теме