Pull to refresh

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

Reading time 10 min
Views 71K
Original author: Dan North
История: Эта статья впервые появилась в журнале 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).
Tags:
Hubs:
+28
Comments 15
Comments Comments 15

Articles