Я руководитель проекта Lisky, интерфейса командной строки, который мы создаем для разработчиков, чтобы они могли взаимодействовать с узлами Lisk и выполнять другие связанные с Lisk функции через командную строку. Недавно мы приняли подход в стиле BDD к модульным тестам в Lisky (начиная с v0.3.0 и далее), и это сообщение в блоге предназначено для всех, кто интересуется тем, как это работает, в частности для всех, кто заинтересован в внесении вклада в кодовую базу Lisky - запросы на включение приветствуются !

О чем мы расскажем в этом посте:

  1. Что такое BDD?
  2. Пошаговое руководство в виде учебника по написанию собственных тестов с использованием этого подхода.
  3. Каковы преимущества и недостатки?

Что такое BDD?

BDD означает разработка, управляемая поведением. Когда дело доходит до определения подходов к автоматическому тестированию, источники могут сильно различаться, но, как здесь понимается, BDD включает идеи от предметно-ориентированного проектирования (DDD) в процесс разработки через тестирование (TDD).

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

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

  1. Написание исполняемой спецификации, состоящей из серии шагов, описанных на языке предметной области, не зависящем от реализации.
  2. Написание тестового кода, который реализует каждый такой шаг атомарно
  3. Написание исходного кода для прохождения тестов (и, таким образом, соответствия спецификации)

Как и в случае с языком Gherkin, наиболее часто используемым для сквозного тестирования, мы разделяем спецификации на

  • Дано (для настройки тестового контекста),
  • Когда (для выполнения тестируемого кода) и
  • Затем (для утверждения) шаги.

Руководство

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

Настраивать

Я предполагаю, что у вас установлены Node и NPM, и вам удобно использовать командную строку. Я использую Node v8.9.0 и NPM v5.5.1, поэтому, если вы столкнетесь с трудностями, указанными ниже, проверьте, помогает ли использование этих точных версий. Приведенный ниже код написан на ES6, так что я предполагаю, что вам это уже удобно.

Создайте каталог и перейдите в него:

Для запуска тестов вам потребуется установить Mocha:

Нет необходимости использовать параметры --save или --save-dev, после установки мы можем запускать Mocha напрямую, используя npx.

Наконец, нам нужны файлы для хранения нашего кода:

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

Счастливый путь

Мы будем использовать подход снаружи внутрь, рассматривая в первую очередь счастливый путь. Outside-in означает, что мы сначала напишем код, с которым действительно хотим работать, и напишем код, который будет работать позже, и только тогда, когда мы будем вынуждены это сделать. Это контрастирует с изнанкой, которая включает в себя попытку предсказать код, который вам понадобится позже, так что когда вы придете писать этот код, у вас уже есть весь код, от которого он зависит. Конечно, вы можете найти подход наизнанку, который вам больше подходит, но снаружи внутрь особенно хорошо работает с BDD.

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

Теперь запуск Mocha в спецификации должен показать нам ожидающий тест, потому что then.itShouldReturn не определен:

Нам нужно определение шага!

Что тут происходит?

  1. Мы экспортируем функцию из then.js для ссылки на спецификацию.
  2. Эта функция утверждает, что возвращаемое значение равно некоторому ожидаемому значению (просто делая то, что подразумевает имя функции).
  3. Возвращаемое значение деструктурируется из тестового контекста (это просто особенность Mocha).
  4. Ожидаемое значение извлекается из заголовка теста с помощью регулярного выражения.

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

Теперь у нас есть провальный тест:

Конечно, returnValue не определено, потому что мы еще не определили его. Нам необходимо разработать нашу спецификацию:

И соответствующее определение шага:

Здесь мы сохраняем возвращаемое значение в тестовом контексте, чтобы шаг Then мог получить к нему доступ позже. Тест не пройден: TypeError: wishHappyBirthday is not a function. Очевидно, что с name и language в какой-то момент придется иметь дело, но прямо сейчас нам нужна функция исходного кода!

Наши тесты проходят:

Но эта функция ужасна, она дает один и тот же результат независимо от имени или языка. Нам нужна более подробная спецификация:

И соответствующие определения шагов:

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

Мы вложили части спецификации, чтобы охватить оба имени на обоих языках (сценарии 2x2). Также обратите внимание, что хотя мы добавили в спецификацию шесть совершенно новых шагов с кучей разных переменных, нам нужно было написать только две функции определения шагов. Вот что я называю СУХИМ!

Мы получаем три ошибки по следующим направлениям:

У нас нет другого выбора, кроме как улучшить исходный код:

И наши испытания проходят!

Несчастливые пути

Так что насчет счастливого пути, что делать, если что-то пойдет не так, например, если кто-то вызовет функцию, используя язык, с которым мы еще не работали? Давайте сначала обновим спецификацию:

Затем определения шагов:

Тесту не важно, известен язык или нет, поэтому мы можем просто указать псевдоним given.anUnknownLanguage для уже написанного нами given.aLanguage шага. Тем не менее, нам необходимо обновить определение нашего when.wishHappyBirthdayIsCalledWithTheNameAndTheLanguage шага, чтобы в случае возникновения ошибки оно сохранялось в тестовом контексте для последующего доступа:

Тест не проходит с TypeError: Cannot read property 'message' of undefined, потому что наша функция вообще не выдает ошибки, не говоря уже о том, что сообщение верное. Пора обновить исходный код:

И все проходит:

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

Каковы преимущества?

  1. Такой подход обеспечивает согласованную структуру / стиль ваших тестов. Спецификации имеют надежный внешний вид, а функции определения шагов в конечном итоге короткие и автономные. Больше никаких спагетти-тестов!
  2. По умолчанию это приводит к атомарным тестам, поэтому ваш набор тестов менее хрупкий.
  3. Написание спецификаций на языке, абстрагированном от реализации теста, позволяет вам думать о точных функциональных возможностях, которые вы хотите, не отвлекаясь на мысли о том, как вы будете тестировать эту функциональность. Это поощряет более сильные и содержательные тесты, которые в конечном итоге должны привести к более надежному исходному коду.
  4. После того, как вы правильно обдумали, какие шаги требуются для некоторого теста, обычно нетривиально написать настоящий тестовый код.
  5. Новичкам в кодовой базе также легче понять написанные вами тесты: вместо того, чтобы делать выводы о значении теста из реализации, новый разработчик просто читает английские предложения, которые объясняют происходящее в виде легко усваиваемых фрагментов. Им помогает тот факт, что описания тестов являются подробными и ясными: более высокая степень повторения в файлах спецификаций - это плата за ясность.
  6. Поскольку каждый шаг определяется в одном месте, этот подход поощряет повторное использование существующих шагов, что приводит к более СУХОЙ кодовой базе. Мы удалили (чистые) сотни строк тестового кода при переходе от нашего старого подхода к тестированию в Lisky.
  7. Рефакторинг часто создает проблемы для тестов: они могут сломаться, что потребует большого количества обновлений повсюду, или в самых коварных случаях они могут потерять смысл, даже если вы этого не заметите. Такой подход значительно упрощает рефакторинг тестового кода - просто внесите изменения в определение соответствующего шага, и все тесты, которые включают этот шаг, отразят обновление.

Какие недостатки?

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

Заключение

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

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

Если вы заинтересованы в сотрудничестве с Lisky, мы будем рады услышать от вас! У нас есть некоторые рекомендации, и мы просим включить в пул-реквесты полное тестовое покрытие (с использованием стиля BDD, описанного в этом сообщении в блоге). Однако следует отметить некоторые отклонения от кода, использованного в этом руководстве:

Удачного тестирования!

дальнейшее чтение