Перевод. Введение в программирование через поведение (BDD)

(Прим. Оригинал статьи на http://dannorth.net/introducing-bdd. Дэн Норт — преподаватель agile методологий разработки. Разрабатывает ПО и учит этому около 20 лет. Создатель собственного агентства по консультированию и разработке ПО. Ввел понятие разработка через поведение (BDD). Опубликовал этот перевод на Хабре здесь.)

История: Эта статья впервые появилась в журнале Better Software в марте 2006. Она была переведена на несколько языков.

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

Чем больше я пользовался TDD, тем больше я понимал, что не столько оттачиваю своё мастерство, достигая новых его вершин, сколько то, что это было движение в слепую. Я помню, как мне все чаще приходила мысль: “Эх, вот бы мне кто-нибудь сказал это раньше!”, чем мысль: “Отлично, дорога ясна”. Я решил, что нужно найти способ обучать TDD, показывающий, как верно работать с ним сразу и без ошибок.

И этот способ — это программирование через поведение. Оно выросло из выработанных agile практик и призвано сделать их доступнее и эффективнее для команд, незнакомых с ними. Со временем, BDD стало включать в себя agile анализ и автоматическое приемочное (прим. acceptance) тестирование.

Выражайте названия тестов (методов) предложениями

Моё открытие, моё радостное “Ага!” я почувствовал, когда мне показали обманчиво простую утилиту agiledox, написанную моим коллегой, Крисом Стивенсоном. Она берёт класс с JUnit тестами и печатает названия их в виде простых предложений. Так тестируемый случай, выглядевший как:

1
2
3
4
5
6
7
8
9
public class CustomerLookupTest extends TestCase {
testFindsCustomerById() {
...
}
testFailsForDuplicateCustomers() {
...
}
...
}

печатается как-то так:

CustomerLookup
  • finds customer by id
  • fails for duplicate customers
  • ...
(Прим. "CustomerLookup [поиск заказчика]: находит заказчика по ID, не находит повторяющихся заказчиков,..")

Слово “test” убрано из названия класса и из методов, и camel запись имен преобразована в обычный текст. Это все, что она делает, но эффект поразительный.

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

Не ошибайтесь, используйте этот простой шаблон предложения для тестов

Потом я нашел интересный шаблон предложения, по которому все имена тестов следует начинать со слова будет (прим. should). Это предложение — This class should do something (прим. Этот класс будет делать что-то) — предполагает, что вы можете написать тест только для текущего класса. Так вы не ошибетесь. Если вы обнаружите, что пытаетесь написать тест, который нельзя так выразить, то, видимо, это поведение другого объекта.

К примеру, однажды, я пишу класс, проверяющий ввод с экрана. Большинство полей — это обычные детали клиента: имя, фамилия и другое; но потом там оказались поле для даты рождения и другое для возраста. Я начал писать, ClientDetailsValidatorTest (прим. валидатор деталей клиента) с такими методами, как testShouldFailForMissingSurname (прим. тест будет падать, если нет фамилии) и testShouldFailForMissingTitle (прим. тест будет падать, если нет заглавия).

Потом я заморочился с вычислением возраста и погряз в мире запутанных бизнес правил: Что если есть дата рождения и возраст и они не совпадают? Что если день рождения сегодня? Какой возраст, если у меня, есть только дата рождения? Я начал писать тогда длинные названия тестов для описания этого поведения, но остановился и решил переместить все это. Поэтому я создал класс, названный AgeCalculator (прим. калькулятор возраста) со своим AgeCalculatorTest (прим. тест для калькулятора возраста). Так все поведение объекта для вычисления возраста переместилось в этот калькулятор и тогда ClientDetailsValidatorу нужен был только один связанный тест, — проверка верного взаимодействия с калькулятором возраста.

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

1
2
3
4
5
6
7
8
public class ClientDetailsValidator {

private final AgeCalculator ageCalc;

public ClientDetailsValidator(AgeCalculator ageCalc) {
this.ageCalc = ageCalc;
}
}

Такой стиль создания объектов вместе, известный как внедрение зависимости, особенно полезен вместе с mock-объектами.

Именуйте тесты ясно: поможет, когда они упадут

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

  • Я написал ошибочный код. Плохой я. Решение: поправить код.
  • поведение было важным, но его переместили куда-то. Решение: переместить тест и, возможно, изменить его.
  • Поведение перестало быть верным: задачи системы изменились. Решение: удалить этот тест.
Последнее часто происходит в agile проектах, вместе с ростом понимания. К несчастью, новички TDD имеют врожденный страх перед удалением тестов, будто это каким-то образом снизит качество кода.

Менее различимый оттенок слова будет (прим. should) становиться понятным, когда сравниваешь его с более формальной альтернативой должен (прим. will или shall). Будет подразумевает, что вы можете сомневаться в тестируемом поведении: “Действительно ли он это будет делать?” Это позволяет легче отличить ситуацию, когда тест действительно падает из-за сделанной в коде ошибки, от той, когда ваши представления о поведении системы уже неверны.

Используйте слово “поведение”, а не “тест”

Итак, теперь у меня был тот инструмент — agiledox — для того, чтобы убрать слово “тест” и использовать то предложение для каждого наименования теста. Тут я понял, что люди, изучая TDD, почти всегда спотыкаются о слово “тест”.

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

Я стал использовать слово “поведение” вместо слова “тест”, когда работал с TDD, и обнаружил, что оно по всей видимости не только подходило, но и магическим образом отпадали все вопросы у учеников. Теперь у меня были ответы на некоторые из тех TDD вопросов. Как проще назвать ваш тест? — это предложение описывающее следующее интересное вам поведение. Вопрос как детально тестировать? становиться чисто теоретическим: вы можете описать только столько поведения, сколько позволяет предложение. Что делать, когда тест падает? — просто следуйте выше описанным шагам: либо вы сделали баг, либо поведение переместилось, либо тест больше не нужен.

Я обнаружил, что думать поведениями, а не тестами, настолько выгодно, что я стал называть TDD тестированием через поведение или BDD.

JBehave акцентируется на поведении, а не на тестировании

Под конец 2003 года, я решил, что пришло время вложить деньги — ну или хотя бы мое время — в то, о чем я говорил. Я начал писать то, что должно было заменить JUnit — JBehave. В нем не было отсылки к тестированию, он замещал это лексикой, построенной вокруг проверяемого поведения. Все это для того, чтобы узнать как будет эволюционировать такой фреймворк, если я буду строго держаться за мои новые мантры о тестировании через поведение. Я также думал, что этот инструмент будет полезен для обучения TDD и BDD без отвлечений на слова производные от слова тест.

Чтобы определить поведение для гипотетического CustomerLookup (прим. поиск заказчика) класса, я бы написал класс для поведения, названный, к примеру, CustomerLookupBehavior (прим. поведение поиска заказчика). Он бы содержал методы, которые начинались бы со слова “будет” (прим. should). Программа запускающая проверку поведения (прим. behaviour runner) тогда создавала бы этот класс поведения и вызывала бы каждый из методов, описывающих поведение по очереди, так же, как это делает JUnit для тестов. Она должна была бы потом отчитываться о прогрессе по ходу исполнения и выдавать итог в конце.

Моя первая цель была сделать так, чтобы JBehave проверял сам себя. Я добавил поведение, которое позволяло этой программе запускать себя. У меня получилось перенести все JUnit тесты в JBehave поведения и получить ту же обратную связь, что и с JUnit.

Определите следующее самое важное поведение

Вскоре после этих экспериментов с JBehave, я стал понимать концепцию бизнес значимости (прим. business value). Конечно, я всегда знал, что я пишу код для чего-то, но я никогда не думал о значимости кода, который я писал сейчас. Мой коллега, бизнес аналитик Крис Маттс, подтолкнул меня к размышлениям о бизнес значимости в контексте тестирования через поведение.

Имея вот эту цель — сделать JBehave самопроверяющим, я обнаружил, что для того, чтобы легче сосредотачиваться, нужно спрашивать себя: “Какая следующая самая важная вещь, которую система не делает?”

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

1
2
3
public void shouldDoX() {
// ...
}

Вот теперь у меня есть ответ на тот вопрос о TDD, а именно, где начать.

Требования — это тоже поведение

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

Ближе к концу 2004 года я как-то рассказывал открытый мной подход, основанный на поведении, Маттсу, и он сказал: “Но это в точности, как анализ”. И после некоторой паузы для обдумывания, мы решили применить все это основанное на поведении мышление для определения требований. Так, если бы мы смогли разработать удобную лексику для аналитиков, тестировщиков, разработчиков и для бизнеса, то мы были бы на верном пути устранения неопределенности и взаимонепонимания, которые появляются, когда техники разговаривают с людьми из бизнеса.

BDD дает “доступный всем язык” для анализа

Где-то в это время Эрик Эванс опубликовал свой бестселлер, книгу “Предметно-ориентированное проектирование” (прим. Domain-Driven Design by Eric Evans). В ней он описывает концепцию моделирования системы, использующую доступный всем язык, основанный на бизнес модели, так чтобы бизнес лексика проникала прямо в код.

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

As a [X]
I want [Y]
so that [Z]
(прим. Будучи X, я хочу Y, так, что произойдет Z.)

где Y — какая-то фича, Z — польза или значение этой фичи и X — человек (или роль), получающий пользу. Преимущество этого предложения в том, что он заставляет вас определить значение разрабатываемой истории во время первого определения ее. Ведь бывает, что когда нет реального бизнес значения истории, то происходит какая-то деградация до чего-то такого: “…Я хочу [какую-то фичу], ну и поэтому [я просто сделаю, да и все, хорошо?].” Наш метод позволяет вынести за рамки проверки эти довольно эзотерические требования.

И с этого старта мы с Маттсом были на пути открытия, того что каждый agile тестировщик и так знает: поведение в пользовательской истории — это просто критерий принятия, а именно, если система выполняет все критерии принятия, то ее поведение верно; если нет — то нет. Поэтому мы создали шаблон предложения для записи критерия оценки пользовательской истории.

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

Имея (прим. given — данное) какой-то контекст,
Когда (прим. when) происходит событие,
Тогда (прим. then) проверить результат.
Чтобы продемонстрировать это, давайте используем классический пример банкомата. Одна из карточек истории могла бы выглядеть так:
+Название: Клиент снимает наличные+
Являясь клиентом,
Я хочу снять деньги в банкомате,
Чтобы мне не ждать в очереди в банке.
Ну, а как мы поймем, что история завершена? У нас несколько сценариев: на счету есть деньги; на счету нет денег, но можно снять в пределах овердрафта; счет превысил овердрафт. Конечно, будут другие сценарии: счет окажется в овердрафте именно с этим снятием, или у банкомата нет денег.

Используя Имея-Когда-Тогда шаблон, первые два сценария могут выглядеть так:

+Сценарий 1: На счету есть деньги+
Имея счет с деньгами
И валидную карточку
И банкомат с наличными
Когда клиент запрашивает наличные
Тогда убедиться, что со счета было списание
И убедиться, что наличные выданы
И убедиться, что карточка возвращена
Заметьте, использование союза и для соединения нескольких начальных условий (прим. given) и результатов (прим. then) облегчает понимание.
+Сценарий 2: Снятие со счета превышает овердрафт+
Имея счет с превышением лимита
И валидную карточку
Когда клиент запрашивает наличные
Тогда убедиться, что сообщение об отказе показано
И убедиться, что наличные не выданы
И убедиться, что карточка возвращена
Оба сценария основаны на одном и том же событии и даже имеют несколько общих исходных условий и результатов. Мы можем извлечь из этого выгоду, используя заново исходные условия, события и результаты.

Сделайте критерий принятий выполняемым

Фрагменты этого сценария — исходные условия, событие и результаты — достаточно малы, чтобы быть запрограммированными. У JBehave есть объектная модель, позволяющая явно соотнести фрагменты сценария с Java классами.

Вы пишите класс, представляющий каждое исходное условие (прим. given) так:

1
2
3
4
5
6
7
8
9
10
public class AccountIsInCredit implements Given {
public void setup(World world) {
...
}
}
public class CardIsValid implements Given {
public void setup(World world) {
...
}
}

и один для того события так:

1
2
3
4
5
public class CustomerRequestsCash implements Event {
public void occurIn(World world) {
...
}
}

и так далее для результатов сценария. JBehave затем связывает это все вместе и выполняет. Он создает “мир”, который где-то существует для хранения ваших объектов, затем JBehave передает его каждому исходному условию (прим. given) по очереди так, что они могут инициализировать мир каким-то известным состоянием. JBehave, затем, просит событие “случиться” в этом мире, которое выполняет заявленное поведение конкретного сценария. И, наконец, JBehave передает управление любому результату, определенному нами в конкретной истории.

Имея классы, представляющие каждый фрагмент сценария, мы можем использовать заново фрагменты для других сценариев или историй. Сначала, эти фрагменты реализованы, используя mock-объекты для установки на счету денег или задания карточке валидности — так мы создаем фундамент для реализации поведения. По мере того, как вы реализуете конкретное приложение, исходные условия и результаты изменяются и начинают использовать реальные классы, созданные вами, и таким образом, к моменту когда сценарий закончен, они становятся верными функциональными тестами от начала до конца.

Настоящее и будущее BDD

После некоторой паузы, JBehave снова активно разрабатывается. Его ядро достаточно закончено и надежно. Следующий шаг — это интеграция с популярными Java IDE такими как IntelliJ IDEA и Eclipse.

Дейв Астель активно продвигал BDD последнее время. Его блог и различные опубликованные статьи спровоцировали шквал активности. Самая заметная — это проект rspec для создания BDD фреймворка на языке Ruby. Я начал работу над rbehave, который будет реализацией JBehave на Ruby.

Мои коллеги, после использования BDD техник в различных реальных проектах, сообщали, что этот способ имеет огромный успех. JBehave подпрограмма для запуска историй — та часть, что проверяет критерий принятия — активно разрабатывается.

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