Введение в BDD
May 28, 2013 | Posted by admin under Практики, Статьи |
Автор: Dan North
http://dannorth.net/introducing–bdd/
Немного истории: Впервые эта статья появилась в журнале Better Software в марте 2006 года. Она переведена на японский Yukei Wachi, корейский HongJoo Lee, итальянский Arialdo Martini и французский Philippe Poumaroux.
Вот какая проблема у меня возникла – используя и обучая таким agile практикам, как разработка через тестирование (test-driven development -TDD), я натыкался на одно и то же непонимание и удивление в совершенно разных проектах. Разработчики хотели знать, где начать, что тестировать и что не тестировать, как много тестировать за один проход, к чему обращаться в тестах и как понять, почему тест обвалился.
Чем больше я углублялся в TDD, тем сильнее я чувствовал, что для меня это скорее не процесс постепенного наращивания мастерства, а серия тупиковых ситуаций. Часто у меня возникала мысль “Если бы мне только кто-нибудь сказал об этом!”, и гораздо реже: “Ура, дверь открылась”. Я решил, что можно представить TDD так, чтобы эта практика приводила к хорошим результатам, и удавалось избежать подводных камней.
Для решения возникающих проблем я создал практику, которая называется «разработка, основанная на функционировании» (behaviour-driven development – BDD). Эта практика эволюционировала из уже установившихся agile практик, и разработана, чтобы сделать их более доступными и эффективными для команд-новичков в agile разработке. Со временем BDD охватила такие области, как agile анализ и автоматизированное приемочное тестирование.
Название тестового метода должно быть предложением
Первый раз я натолкнулся на это, когда смотрел очень простую утилиту agiledox, которую написал мой коллега Крис Стивенсон (Chris Stevenson). Она берет Junit-овский тестовый класс и печатает имена методов в виде простых предложений, т.е. для test case-а, который выглядит следующим образом:
public class CustomerLookupTest extends TestCase { | |
testFindsCustomerById() { |
… | |
} |
testFailsForDuplicateCustomers() { | |
… |
} | |
… |
} |
выдается примерно следующее:
CustomerLookup | |
– finds customer by id |
– fails for duplicate customers | |
– … |
Слово “test” убрано из имени класса и методов, а названия превращены в обычный текст. Вот все, что делает эта утилита, но эффект просто удивительный.
Разработчики поняли, что таким образом часть документации будет написана автоматически, и начали писать названия тестовых методов в виде предложений. Они поняли, что когда пишут название метода на языке бизнес домена, сгенерированные документы имеют смысл для бизнес пользователей, аналитиков и тестировщиков.
Простой шаблон делает тестовые методы более определенными
Я пришел к тому, что в названия тестовых методов стоит включать слово «должен» (“should”). Шаблон – Класс должен делать что-то (The class should do something) – означает, что вы создаете тесты для данного класса и таким образом вы фокусируетесь на этом. Если оказывается, что вы пишете тест, чье имя не соответствует этому шаблону, это может означать, что поведение относится к чему-то другому.
Вот пример: я писал класс, который валидирует ввод с экрана. Большей частью поля – стандартные свойства пользователя: имя, фамилия, и т.д. Кроме того, было поле даты рождения и поле для указания возраста. Я начал писать класс ВалидаторСвойствКлиентаТест (ClientDetailsValidatorTest) с такими методами как тестДолженЗавершитьсяОшибкойЕслиФамилияНеУказана (testShouldFailForMissingSurname) и тестДолженЗавершитьсяОшибкойЕслиПрефиксНеУказан (testShouldFailForMissingTitle).
Затем я перешел к подсчету возраста и оказалось, что здесь много неясностей: что если возраст и дата рождения указаны, но не соответствуют друг другу? Что если дата рождения – сегодня? Как подсчитать возраст, если у меня есть только день и месяц рождения? Я начал писать очень сложные названия методов, чтобы отразить это поведение, и в конце концов пришел к тому, что нужно решать это как-то иначе. Я решил создать класс с названием КалькуляторВозраста (AgeCalculator) со своим методом КалькуляторВозрастаТест (AgeCalculatorTest). Все поведение, касающееся подсчета возраста, было перенесено в калькулятор, поэтому валидатору нужен только один тест для подсчета возраста, чтобы убедиться, что он правильно взаимодействует с калькулятором.
Если класс делает больше, чем одну вещь, обычно для меня это знак создать другие классы, в которых будет выполняться часть работы. Я создаю новый сервис как интерфейс, название которого говорит, что он делает, и передаю его в конструкторе класса:
public class ClientDetailsValidator { | |
private final AgeCalculator ageCalc; | |
public ClientDetailsValidator(AgeCalculator ageCalc) { | |
this.ageCalc = ageCalc; |
} | |
} |
Такое связывание объектов, известное как внедрение зависимости (dependency injection) особенно полезно в сочетании с мock-объектами.
Выразительное название очень помогает, когда тест обрушился
Вскоре я понял, что если поменял код, и в результате тест обрушился, я могу посмотреть на название тестового метода и понять, какое поведение требовалось от кода. Обычно происходит одно из трех:
- Я внес ошибку. Это плохо. Решение: исправить ошибку.
- Предполагаемое поведение по-прежнему актуально, но куда-то передвинулось. Решение: Передвинуть тест и возможно поменять его.
- Описанное поведение уже считается некорректным – изменилось понимание. Решение: Удалить тест.
Последний вариант вероятен в agile проектах, по мере того, как понимание системы эволюционирует. К сожалению, те, кто только начинают использовать TDD, подвержены необъяснимому страху перед удалением тестов, как будто это как-то ухудшает качество их кода.
Более тонкий аспект слова должен (should) становится очевидным при сравнении с более формальными альтернативами будут (will) или будем (shall). Должен (should) неявно задает вопрос о том, верно ли условие теста: “Должно это выполняться? Действительно?” Это помогает решить, почему тест завершился ошибкой – из-за дефекта или просто потому, что ваши предыдущие предположения о поведении системы стали теперь некорректными.
Слово “поведение” более полезное, чем “тест”
Итак, у меня появился инструмент, agiledox, для удаления слова “тест”, и шаблон для имен тестовых методов. Я понял, что во многом непонимание TDD возвращается к слову “тест”.
Не то, чтобы тестирование и TDD не связаны, полученный набор методов – это эффективный способ удостовериться в том, что ваш код работает. Однако, если методы не всесторонне описывают поведение системы, то ощущение безопасности, появившееся у вас, обманчиво.
Я стал использовать слово «поведение» вместо «тест» при работе с TDD, и оказалось, что оно не просто лучше подходит, но и магическим образом решает целую категорию вопросов, возникших в процессе коучинга. У меня появились ответы на многие вопросы. Как назвать тест? – Это должно быть предложение, описывающее интересующее вас поведение. Сколько нужно проверить с помощью теста? – Нужно проверить поведение, описание которого укладывается в одно предложение. Что делать, если тест провалился? – Двигайтесь по процессу, описанному выше, т.е. либо вы внесли ошибку, либо описанное поведение теперь реализовано в другом месте, либо тест уже не актуален.
Я посчитал переход от мышления в формате тестов к мышлению в формате поведения настолько значительным, что вместо TDD стал использовать название BDD, т.е. разработка на основе поведения (behaviour- driven development).
JBehave подчеркивает значение слова «поведение», в отличие от «тестирования»
В конце 2003 года я решил вложить свои деньги в эту идею. Я начал писать JBehave, взамен JUnit. Все ссылки на тестирование убирались и заменялись словарем, построенным вокруг верификации поведения. Я сделал это, чтобы посмотреть, насколько эволюционирует фреймворк, если я буду строго придерживаться идеи разработки на основе поведения. Я также думал о том, что это будет ценный обучающий инструмент, который поможет познакомиться с TDD и BDD, и не запутаться со словарем, построенным на тестировании.
Чтобы определить поведение гипотетического класса ПоискКлиентов (CustomerLookup), я бы написал поведенческий класс под названием ПоведениеПоискаКлиентов (CustomerLookupBehaviour). Он бы содержал методы, которые начинались бы со слова «должен» («should»). Инструмент, проверяющий поведение, создал бы объект поведенческого класса и вызвал бы каждый из поведенческих методов по очереди, так же как это делает JUnit с тестами. Показывал бы прогресс и в конце выдал бы отчет.
Первым этапом для меня было сделать JBehave самоверифицируемым. Я добавил поведение, которое позволило ему запускать самого себя. Я смог перевести все JUnit-овские тесты в поведения для JBehave и получил с помощью JBehave тот же результат, что и с помощью JUnit.
Определение наиболее важного поведения
Затем я узнал о концепции бизнес ценности. Конечно, я всегда понимал, что я пишу программного обеспечение для чего-то, но я никогда реально не задумывался о ценности кода, который я в каждый момент пишу. Мой коллега, бизнес аналитик Крис Мэттс (Chris Matts), заставил меня подумать о бизнес ценности в контексте разработки на основе поведения.
Размышляя об этом, у меня появилась цель сделать JBehave самобазирующимся (self- hosting). Для того, чтобы оставаться сконцентрированным, мне показалось действительно полезным задать вопрос: Какую важную вещь система не делает?
Для ответа на этот вопрос вам нужно определить ценность фич, которые вы еще не реализовали, и приоритезировать их. Кроме того, это поможет вам сформулировать имя поведенческого метода: Система не делает X (где X это какое-то значимое поведение), и X это важно, что в итоге означает, что система должна делать X. Поэтому название вашего следующего поведенческого метода просто:
public void shouldDoX() { | |
// … |
} |
Теперь у меня появился ответ на еще один вопрос, связанный с TDD, а именно, – где начинать.
Требования – это тоже поведение
Итак, у меня появился фреймворк, который помог мне понять, и что даже важнее – объяснить, как работает TDD, а также подход, который позволил мне избежать перечисленных выше подводных камней.
В конце 2004 года, когда я разъяснял словарь мышления, основанного на функционировании, Мэттсу, он сказал: “Но это выглядит как анализ”. Мы долго это обдумывали и решили применить все это для определения требований. Если мы сможем разработать согласованный словарь для аналитиков, тестировщиков, разработчиков и бизнеса, то это поможет избежать неоднозначностей и несогласованностей, которые возникают, когда люди от разработки общаются с бизнесом.
BDD предоставляет «общий язык» для анализа
Примерно в это время Эрик Эванс (Eric Evans) опубликовал свой бестселлер «Проблемно-ориентированное проектирование» («Domain-Driven Design»). В этой книге он описывает концепцию моделирования системы с использованием общего языка, основанном на бизнес области, так что бизнес словарь проникает в код.
Мы с Крисом поняли, что пытаемся создать общий язык для процесса анализа как такового! У нас была хорошая стартовая точка. В компании использовался шаблон для историй, который выглядел так:
Как (As a) [X]
Я хочу (I want) [Y]
Чтобы (so that) [Z]
где Y – это некая функциональность (фича), Z – это преимущество, которое мы от нее получаем, или ценность, а X – это человек (или роль), которая получит ценность от фичи. Сила этого шаблона в том, что он заставляет вас определить ценность от поставки истории, когда вы впервые определяете ее. Если история не имеет реальной бизнес ценности, то часто ее описание превращается во что-то вроде ” . . . Я хочу [некая фича] чтобы [Мне просто нужно, понятно?].” Такой шаблон также помогает выявить некоторые скрытые требования.
С этой точки Мэттс и я начали приходить к тому, что уже знает каждый agile тестировщик: поведение истории – это просто ее критерии приемки. Если система проходит все критерии приемки, значит она ведет себя правильно; в противном случае – нет. Итак, мы создали шаблон для выявления приемочных критериев истории.
Шаблон должен был быть достаточно свободным, чтобы не давать аналитикам ощущения сложности или ограниченности , вместе с тем он должен был быть достаточно структурированным, так чтобы можно было разбить историю на составные части и автоматизировать их. Мы начали описывать критерии приемки в терминах сценариев, в такой форме:
Допустим (Given) некоторый начальный контекст (данность),
Если (When) происходит событие,
То (then) убедится, что получены некоторые результаты.
Чтобы проиллюстрировать, давайте возьмем классический пример с банкоматом. Одна из карточек историй может выглядеть так:
+Название: Клиент изымает наличные+
Как клиент,
Я хочу получить наличные из банкомата,
чтобы мне не пришлось стоять в очереди в банке.
Итак, как мы узнаем, что поставили эту историю? Нужно рассмотреть несколько сценариев: на счету есть деньги, счет может быть превышен, но не вышел за границы лимита, счет может быть превышен за рамки лимита. Конечно, будут и другие сценарии, например, это счет по кредиту и текущее снятие приводит к превышению лимита, или в банкомате недостаточно наличности.
Используя шаблон допустим-если-то, можно представить сценарии так:
+Сценарий 1: На счету есть деньги+
Допустим на счету есть деньги
И Карточка валидная
И в банкомате есть наличность
Если Клиент запрашивает наличность
То Убедиться в том, что сумма вычтена со счета
И убедиться в том, что деньги выданы
И убедиться в том, что карточка возвращена
Обратите внимание на использование “и” для объединения начальных условий или результатов естественным образом.
+Сценарий 2: счет превышен за рамки лимита +
Допустим счет превышен
И карточка валидная
Если клиент запрашивает наличность
То убедиться в том, что показано сообщение об отказе
И убедиться в том, что наличность не выдана
И убедиться в том, что карточка возвращена
Оба сценария основаны на одном событии и имеют некоторые общие исходные условия и результаты. Хочется ввести механизм повторного использования частей сценариев.
Приемочные критерии должны быть выполняемыми
Фрагменты сценария – исходные условия, событие, результаты – достаточно хорошо гранулированы для представления в коде. JBehave определяет объектную модель, которая позволяет отобразить фрагменты сценария в Java классах.
Вы пишете класс для каждого исходного условия:
public class НаСчетуЕстьДеньги implements Given { | |
public void установить(Мир мир) { |
… | |
} |
} | |
public class КарточкаВалидная implements Given { |
public void установить(Мир мир) { | |
… |
} | |
} |
И один для события:
public class КлиентЗапрашиваетНаличность implements Event { | |
public void происходитВ(Мир мир) { |
… | |
} |
} |
И то же самое для результатов. JBehave связывает все это и выполняет. Он создает “мир”, для хранения ваших объектов, и передает его в каждое из условий по очереди, чтобы они установили в этом мире нужные состояния. Затем JBehave говорит событию “произойти в” мире, в результате чего выполняется поведение сценария. И наконец, JBehave передает управление результатам истории.
То, что у нас есть класс, представляющий каждый фрагмент, позволяет нам повторно использовать фрагменты в других сценариях или историях. Сначала в фрагментах для описания валидной карточки или наличия денег на счету используются заглушки. По мере реализации приложения, фрагменты обновляются, в них начинают использовать реальные классы, таким образом, когда сценарии реализованы, они становятся отличными функциональными тестами.
Настоящее и будущее BDD
После небольшого перерыва JBehave опять в активной разработке. Ядро фактически завершено и устойчиво. Следующий шаг – интеграция с популярными Java IDE, такими как IntelliJ IDEA и Eclipse.
Дэйв Астелз (Dave Astels) стал активно продвигать BDD. Его блог и статьи спровоцировали взрыв активности, особенно надо отметить проект rspec , в котором фреймворк BDD воспроизводится на Ruby. Я начал работать над rbehave, который станет реализацией JBehave на Ruby.
Многие мои коллеги стали использовать методы BDD на разных реальных проектах, и нашли эти методы очень успешными. В активной разработке компонент JBehave для запуска историй, та часть, которая обеспечивает проверку приемочных критериев.
Этот обзор рассказывает о том, что имея специальный редактор, аналитики и тестировщики могли бы преобразовывать истории в поведенческие классы, в которых был бы использован язык бизнес области.
BDD развивается с помощью множества людей, и я приношу свою благодарность им всем.
-
Vadim Musteata
-
34