Асинхронное кодирование сложно сделать правильно, а когда дело доходит до тестирования асинхронного кода, довольно легко все испортить, если вы неосторожны или плохо понимаете разницу между синхронным и асинхронным кодом.

Либо используйте мокко в бэкэнде с node.js, либо жасмин во фронтенде с кармой, если вы никогда не выполняли асинхронное тестирование до этого, у вас будет много проблем с правильным решением.

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

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

С другой стороны, это может - или не - привести к ложноотрицательному в другом тесте или к странным исключениям, которые кажутся бессмысленными.

При работе с асинхронным кодом не только действительно легко испортить ваши спецификации; кроме того, вы получите недетерминированные выходные данные: иногда пакет проходит, иногда нет. И когда это не удается, неудачный тест может не совпадать между каждым запуском. Обычно набор тестов проходит локально, но дает сбой на вашем CI-сервере.

Когда у вас есть такие симптомы, у вас наверняка есть спецификация - или несколько - которые неправильно обрабатывают ее асинхронность.

Эта проблема

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

Так же, как этот фрагмент node.js:

никогда ничего не будет записывать, код вроде этого:

не запустит expect(true).to.be.false; строку до конца теста.

Вдобавок ко всему, в большинстве наборов тестов javascript спецификации без каких-либо ожиданий просто проходят. Этот фрагмент:

Сгенерирует этот вывод в мокко:

the silliest test suite ever
    ✓ a spec with no expectation will always pass
1 passing (5ms)

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

Однако это поведение по умолчанию может привести к серьезным проблемам, когда вы имеете дело с утверждениями внутри асинхронных обратных вызовов:

Этот тест, который, по-видимому, должен просто провалиться, на самом деле проходит. Это результат:

a false positive
    ✓ an expect inside asynchronous code will be ignored
1 passing (8ms)

Поскольку ожидание находится в пределах 1 мсsetTimeout, оно не будет выполняться до следующего тика: когда спецификация завершится, код ожидания все еще отсутствует, поэтому тест просто проходит, поскольку это значение по умолчанию для спецификации без каких-либо ожиданий внутри.

решение

Решить эту проблему очень просто: как в мокко, так и в жасмине, вы можете просто передать дополнительный параметр в спецификацию it, обычно называемую done-.

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

Как и ожидалось, этот тест не прошел.

Кроме того, в мокко вы также можете вернуть обещание в своей спецификации, чтобы заставить тестовый движок дождаться разрешения, прежде чем завершить спецификацию:

Mocha, как и ожидалось, провалит этот тест.

Попадания

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

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

Представим, что у нас есть такой набор:

Итак, первая спецификация, get(), явно неверна. Однако пакет выдаст такой результат:

ACME.com
✓ get ()
✓ getLolz () (104 мс)
✓ getFruit () (102 мс)

3 прохода (216 мс)

Когда это неправильно. Если вам повезло и вы имеете дело с набором программ, который ожидает запуска каждого обратного вызова в стеке, вы получите предупреждение о том, что первое ожидание не выполняется, когда оно терпит неудачу (текущая версия mocha сделает это, т. Е.). Если вы этого не сделаете, ваши тесты с радостью пройдут. Если вы используете набор жасмина внутри кармы, это может зависеть от конфигурации вашей кармы.

Что ж, давайте представим, что это спецификации вашей кодовой базы. Кажется, все работает нормально, так как все тесты прошли. Теперь мы добавляем новую функцию getPlayer() и тоже ее тестируем:

Теперь, в зависимости от используемой библиотеки тестирования и ее версии, мы получим какое-то странное сообщение об ошибке. Например, легко получить что-то вроде:

✓ get ()
✓ getLolz () (112 мс)
✓ getFruit () (102 мс)
1) getPlayer ()

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

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

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

Здесь начинают происходить странные вещи:

1- Если мы удалим четвертый тест, набор пройден.

2- Если мы удалим первый, комплект пройдет.

3- Если мы добавим какую-либо спецификацию между первой и четвертой, выполнение которой займет более 800 мс, этот тест завершится неудачно.

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

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

(

просто, если вы не знаете, в библиотеках тестирования обычно есть простые способы игнорировать тесты или запускать некоторые тесты изолированно:

  • mocha и jest имеют it.only() и describe.only(), чтобы гарантировать, что спецификация или блок описания запускаются изолированно.
  • У Жасмин есть fit() и fdescribe() («f» означает «фокус» afaik).
  • Большинство библиотек позволяют xit() или xdescribe() игнорировать тест или описывать блок. Кроме того, у мокко есть .skip() метод пропуска блока или теста.

)

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

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

Очевидно, что здесь нужно пройти спецификацию по спецификации и проверить, правильно ли работают асинхронные утверждения: отсутствие обратного вызова done (или возвращенного обещания в мокко) является сильным сигнал о том, что что-то сломано.

Плохие обещания

Однако это не единственная возможная проблема. Работая с обещаниями, мы могли нарушить тесты из-за ошибок при создании цепочек обещаний:

Здесь мы присоединяем .then сразу после acme.getSomething (): при этом мы заключаем ожидание в простой обратный вызов - он просто вызывается внутри обработчика .then- и отсоединив его от основной цепочки обещаний. Как следствие этого, обратный вызов done будет выполняться перед утверждением, и тест будет пройден, даже если он не работает («abracadabra» здесь не является ожидаемым результатом).

Итак, просто проверка выполнения параметра передается в спецификацию, а затем вызывается в спецификации - это не единственное, что вам нужно проверить.

Чтобы правильно не работать, спецификация должна быть написана так:

Общее состояние

Говорят, что тесты всегда нужно изолировать: это действительно хороший совет в целом. Кроме того, при асинхронном тестировании необходимо правильно изолировать все тесты друг от друга. Если некоторые из ваших тестов зависят от некоторого состояния, установленного предыдущим тестом, вы не можете запускать тесты изолированно. Когда вы не можете запустить сбойную спецификацию изолированно, вы не можете определить, отказывает ли она сама по себе, по предыдущему тесту, изменяющему состояние, или по другому, который создает асинхронное отложенное исключение. Слишком много кандидатов: никогда не делитесь состоянием между своими спецификациями, и вам никогда не придется учитывать зависимости между тестами при отладке неудачного набора.

So

Я очень надеюсь, что эта история поможет кому-то там: когда я только начал проводить js-тестирование, я почти не имел представления о странностях асинхронных API, обратных вызовов, обещаний и всего прочего, и мне было очень плохо с этим справляться. Асинхронное кодирование сложно, а асинхронное тестирование сложнее, и такие проблемы при создании TDD javascript довольно распространены, поэтому я действительно ожидаю, что это может быть кому-то полезно :-D!