История: Эта статья впервые появилась в журнале Better Software в марте 2006. Она была переведена на несколько языков.
Однажды я столкнулся с проблемой. Обучая разработчиков практикам agile программирования, таким как TDD в различных проектах, я часто встречал непонимание и растерянность. Они хотели знать, где начать, что тестировать, а что не тестировать, как много тестировать за раз, как называть тесты и как понять, почему тесты падают.
Чем больше я пользовался TDD, тем больше я понимал, что нестолько оттачиваю своё мастерство, достигая новых его вершин, сколько то, что это было движение в слепую. Я помню, как мне все чаще приходила мысль: «Эх, вот бы мне кто-нибудь сказал это раньше!», чем мысль: «Отлично, дорога ясна». Я решил, что нужно найти способ обучать TDD, показывающий, как верно работать с ним сразу и без ошибок.
И этот способ — это программирование через поведение. Оно выросло из выработанных agile практик и призвано сделать их доступнее и эффективнее для команд, незнакомых с ними. Со временем, BDD стало включать в себя agile анализ и автоматическое приемочное (прим. acceptance) тестирование.
Выражайте названия тестов (методов) предложениями
Моё открытие, моё радостное «Ага!» я почувствовал, когда мне показали обманчиво простую утилиту agiledox, написаную моим коллегой, Крисом Стивенсоном. Она берёт класс с JUnit тестами и печатает названия их в виде простых предложений. Так тестируемый случай, выглядивший как:
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`у нужен был только один связанный тест, — проверка верного взаимодействия с калькулятором возраста.
Если какой-то класс делает, что-то кроме одного, то это обычно признак того, что я должен насоздавать еще классов для этого другого. Я создаю, новый сервис в виде интерфейса, описывающего, что он делает, и передаю его конструктору первого класса:
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, поэтому ваш следующий метод поведения вот такой:
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) так:
public class AccountIsInCredit implements Given {
public void setup(World world) {
...
}
}
public class CardIsValid implements Given {
public void setup(World world) {
...
}
}
и один для того события так:
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 эволюционировал с помощью многих людей и я выражаю огромную благодарность им.
Прим. Дон Норт — преподаватель agile методологий разработки. Разрабатывает ПО и учит этому около 20 лет. Создатель собственного агенства по консультированию и разработке ПО. Ввел понятие разработка через поведение (BDD).
Автор: w1ld