Я руководитель проекта Lisky, интерфейса командной строки, который мы создаем для разработчиков, чтобы они могли взаимодействовать с узлами Lisk и выполнять другие связанные с Lisk функции через командную строку. Недавно мы приняли подход в стиле BDD к модульным тестам в Lisky (начиная с v0.3.0 и далее), и это сообщение в блоге предназначено для всех, кто интересуется тем, как это работает, в частности для всех, кто заинтересован в внесении вклада в кодовую базу Lisky - запросы на включение приветствуются !
О чем мы расскажем в этом посте:
- Что такое BDD?
- Пошаговое руководство в виде учебника по написанию собственных тестов с использованием этого подхода.
- Каковы преимущества и недостатки?
Что такое BDD?
BDD означает разработка, управляемая поведением. Когда дело доходит до определения подходов к автоматическому тестированию, источники могут сильно различаться, но, как здесь понимается, BDD включает идеи от предметно-ориентированного проектирования (DDD) в процесс разработки через тестирование (TDD).
Об этих концепциях написано так много в других местах, поэтому я не буду вдаваться в подробности здесь. Если какой-либо из этих терминов является для вас новым, не беспокойтесь об этом - самый простой способ понять, как работает этот подход, - это просто проработать примеры в приведенном ниже руководстве.
Вкратце: TDD - это подход к разработке программного обеспечения, при котором разработчик сначала пишет тесты, определяющие желаемое поведение, а затем пишет код, который проходит тесты. DDD - это подход, который подчеркивает согласованный, не зависящий от реализации язык предметной области. Таким образом, принятая нами форма BDD включает следующие три этапа:
- Написание исполняемой спецификации, состоящей из серии шагов, описанных на языке предметной области, не зависящем от реализации.
- Написание тестового кода, который реализует каждый такой шаг атомарно
- Написание исходного кода для прохождения тестов (и, таким образом, соответствия спецификации)
Как и в случае с языком Gherkin, наиболее часто используемым для сквозного тестирования, мы разделяем спецификации на
- Дано (для настройки тестового контекста),
- Когда (для выполнения тестируемого кода) и
- Затем (для утверждения) шаги.
Руководство
Хорошо, давайте попробуем написать тесты для функции, которая принимает имя и язык и поздравляет этого человека с днем рождения на выбранном языке. Есть репо-компаньон со всем кодом, описанным в этом блоге, на случай, если вы заблудитесь в любой момент. История коммитов соответствует описанному здесь прогрессу, поэтому вы можете вернуться именно к той точке, которая вам нужна.
Настраивать
Я предполагаю, что у вас установлены Node и NPM, и вам удобно использовать командную строку. Я использую Node v8.9.0 и NPM v5.5.1, поэтому, если вы столкнетесь с трудностями, указанными ниже, проверьте, помогает ли использование этих точных версий. Приведенный ниже код написан на ES6, так что я предполагаю, что вам это уже удобно.
Создайте каталог и перейдите в него:
Для запуска тестов вам потребуется установить Mocha:
Нет необходимости использовать параметры --save
или --save-dev
, после установки мы можем запускать Mocha напрямую, используя npx.
Наконец, нам нужны файлы для хранения нашего кода:
В приведенных ниже блоках кода я помещаю имя редактируемого файла в комментарий вверху и указываю, когда я убираю код, с помощью // ...
комментария.
Счастливый путь
Мы будем использовать подход снаружи внутрь, рассматривая в первую очередь счастливый путь. Outside-in означает, что мы сначала напишем код, с которым действительно хотим работать, и напишем код, который будет работать позже, и только тогда, когда мы будем вынуждены это сделать. Это контрастирует с изнанкой, которая включает в себя попытку предсказать код, который вам понадобится позже, так что когда вы придете писать этот код, у вас уже есть весь код, от которого он зависит. Конечно, вы можете найти подход наизнанку, который вам больше подходит, но снаружи внутрь особенно хорошо работает с BDD.
Обращаясь к счастливому пути, мы начинаем с определения того, что должно произойти, если все пойдет по плану:
Теперь запуск Mocha в спецификации должен показать нам ожидающий тест, потому что then.itShouldReturn
не определен:
Нам нужно определение шага!
Что тут происходит?
- Мы экспортируем функцию из
then.js
для ссылки на спецификацию. - Эта функция утверждает, что возвращаемое значение равно некоторому ожидаемому значению (просто делая то, что подразумевает имя функции).
- Возвращаемое значение деструктурируется из тестового контекста (это просто особенность Mocha).
- Ожидаемое значение извлекается из заголовка теста с помощью регулярного выражения.
Последний пункт здесь позволяет нам представить конкретные примеры определенных значений в нашей спецификации, которые затем используются непосредственно в наших тестах, поэтому легко увидеть, работает ли спецификация с реалистичными значениями, и нам не нужно беспокоиться о сохранении избыточные определения, синхронизированные друг с другом. Это также означает, что этот шаг готов к повторному использованию в другом тесте с другим строковым значением.
Теперь у нас есть провальный тест:
Конечно, 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
, потому что наша функция вообще не выдает ошибки, не говоря уже о том, что сообщение верное. Пора обновить исходный код:
И все проходит:
Очевидно, что вы можете сделать гораздо больше с точки зрения проверки этой функции (отсутствующие имена, имена с неправильным типом и т. Д.), Но мы оставим это руководство здесь.
Каковы преимущества?
- Такой подход обеспечивает согласованную структуру / стиль ваших тестов. Спецификации имеют надежный внешний вид, а функции определения шагов в конечном итоге короткие и автономные. Больше никаких спагетти-тестов!
- По умолчанию это приводит к атомарным тестам, поэтому ваш набор тестов менее хрупкий.
- Написание спецификаций на языке, абстрагированном от реализации теста, позволяет вам думать о точных функциональных возможностях, которые вы хотите, не отвлекаясь на мысли о том, как вы будете тестировать эту функциональность. Это поощряет более сильные и содержательные тесты, которые в конечном итоге должны привести к более надежному исходному коду.
- После того, как вы правильно обдумали, какие шаги требуются для некоторого теста, обычно нетривиально написать настоящий тестовый код.
- Новичкам в кодовой базе также легче понять написанные вами тесты: вместо того, чтобы делать выводы о значении теста из реализации, новый разработчик просто читает английские предложения, которые объясняют происходящее в виде легко усваиваемых фрагментов. Им помогает тот факт, что описания тестов являются подробными и ясными: более высокая степень повторения в файлах спецификаций - это плата за ясность.
- Поскольку каждый шаг определяется в одном месте, этот подход поощряет повторное использование существующих шагов, что приводит к более СУХОЙ кодовой базе. Мы удалили (чистые) сотни строк тестового кода при переходе от нашего старого подхода к тестированию в Lisky.
- Рефакторинг часто создает проблемы для тестов: они могут сломаться, что потребует большого количества обновлений повсюду, или в самых коварных случаях они могут потерять смысл, даже если вы этого не заметите. Такой подход значительно упрощает рефакторинг тестового кода - просто внесите изменения в определение соответствующего шага, и все тесты, которые включают этот шаг, отразят обновление.
Какие недостатки?
- Написать хорошие спецификации сложно. На самом деле это верно и для других подходов к тестированию, но, возможно, это становится более очевидным, когда вы выполняете BDD.
- Иногда бывает трудно сказать, что вы уже определили шаг, который используете в новом тесте. Это можно смягчить, разделив определения шагов на модули, чтобы вы могли легко просмотреть короткий список потенциально применимых определений шагов перед написанием нового.
- Это немного нетрадиционно, поэтому разработчикам, плохо знакомым с этим подходом, может потребоваться некоторое время, чтобы привыкнуть к нему.
Заключение
Такой подход BDD к тестированию является для нас относительно новым, и мы все еще привыкаем к нему работать. По мере того, как мы становимся более удобными, мы открываем для себя больше шаблонов, методов управления базой кода и улучшаем подходы к созданию шагов. Но мы уже видели преимущества с точки зрения более четких тестов и более сжатой тестовой кодовой базы.
Я считаю хорошим знаком то, что когда дело доходит до написания тестов в других проектах, которые не использовали этот подход, теперь он кажется удручающе неструктурированным, как будто он предлагает вам писать ленивые тесты.
Если вы заинтересованы в сотрудничестве с Lisky, мы будем рады услышать от вас! У нас есть некоторые рекомендации, и мы просим включить в пул-реквесты полное тестовое покрытие (с использованием стиля BDD, описанного в этом сообщении в блоге). Однако следует отметить некоторые отклонения от кода, использованного в этом руководстве:
Удачного тестирования!
дальнейшее чтение
- Константин Кудряшов на Моделирование на примере (и видео)
- Тестирование README в проекте Lisky