Написание модульных тестов для асинхронного недетерминированного кода может оказаться сложной задачей. Как вы проводите тестирование функций, использующих сетевые вызовы или чтение/запись в файловую систему? Мокающие библиотеки, такие как testdouble или sinon, могут помочь. Но как насчет тестирования подобных сценариев, использующих цикл обработки событий Node? Рассмотрим следующий пример:

Чтобы протестировать эту функцию и охватить все основы, нам нужно иметь возможность имитировать события connect, error и timeout. Для этого нам нужно заглушить методы connect и setTimeout. Мы можем добиться этого, используя EventEmitter и заглушая методы, присоединяя их к эмиттеру.

Давайте настроим нашу тестовую установку:

npm init -y
npm install -D mocha chai testdouble
mkdir test && touch test/ping.js

Мы также хотим настроить скрипт test в package.json следующим образом:

"scripts": {
  "test": "mocha test/*"
},
...

Далее давайте настроим и обсудим конфигурацию для наших тестов:

Здесь многое происходит. Во-первых, мы вызываем td.replace() в модуле net, дозапроса кода, который мы хотим протестировать. Это очень важно и является странной особенностью testdouble, а также тем, как Node кэширует необходимые модули. Метод replace аналогичен встроенному методу require, за исключением того, что он заглушает весь код в этом модуле.

Далее мы настраиваем некоторые хуки, которые запускаются до и после каждого теста внутри блока describe. В хуке beforeEach мы создаем нашего «клиента». Чтобы сымитировать net.Socket, мы можем просто создать эмиттер событий и прикрепить к нему методы-заглушки. Это дает нам возможность генерировать прослушиваемые события, имитируя методы connect и setTimeout. Когда наш код запрашивает новый объект сокета, мы просто имитируем его, возвращая этот пользовательский объект client. Это именно то, что делает вызов td.when(new net.Socket()) внутри нашего хука. Нам также нужно удалить все прослушиватели событий, когда «клиент» будет уничтожен. Наконец, после запуска каждого теста нам нужно сбросить testdouble, вызвав td.reset(), чтобы предотвратить загрязнение теста.

Напишем наш первый тест:

Во-первых, мы заглушаем метод client.connect, сопоставляя любые входные данные и имитируя успешное соединение, генерируя событие connect. Затем мы вызываем наш метод ping, ждем ответа и утверждаем, что ответ разрешается в true. Блок catch гарантирует, что мы обработаем случай, когда обещание отклоняется из-за ошибки, если было сгенерировано событие error.

Я оставлю это вам, чтобы справиться с другими базовыми случаями, когда есть тайм-аут или ошибка 😉. Если хотите, можете посмотреть мой код здесь.