Пользователь
0,0
рейтинг
24 марта 2014 в 18:40

Разработка → Введение в программирование через поведение (BDD) перевод tutorial

История: Эта статья впервые появилась в журнале 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).
Перевод: Dan North
w1ld @w1ld
карма
10,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (15)

  • –16
    Во-первых было бы неплохо привести ссылку на оригинал, во вторых — статья не нова, да и на хабре не раз обсуждалась данная тема. Извините.
    • +9
      Ссылка на оригинал средствами хабра указывается внизу статьи обычно, этот случай не исключение.
    • +1
      Ссылка есть на обычном месте — внизу статьи нажимаете на имя автора.

      Да, действительно статья довольно старая (несколько лет ей), но она именно вводит это понятие, BDD. Дон Нортон описывает, что его привело к этому, с какими проблемами он столкнулся. Он описывает те принципы, которые заложены до сих пор в этой методологии. Статья ценна этим, ведь лучше человека, который открыл этот метод, никто не расскажет.

      Мне эта статья помогла уложить по полочкам все то, что лежит в этом подходе. Раньше я скептически к этому относился и использовал неправильно BDD — вносил множество деталей. Теперь я лучше понимаю этот подход.

      Надеюсь она поможет и другим.
      • 0
        Извиняюсь, не знал. Спасибо за пояснение о ссылках.
      • +1
        Автор не Дон Норт и тем бодее не Дон Нортон. Он Дэн Норт (типа Денис :))
        • 0
          Спасибо, исправил.
  • 0
    Спасибо за статью. Раз уж в заголовке есть слово «введение» позволю себе нубский вопрос к аудитории. Мы довольно давно подсели на agile, но вот дзен все никак не наступит. Вцелом получается, что как только мы начинаем писать не технические истории, а именно behavior (As X I want Y so that Z) и пытаемся протянуть все до уровня тестов, получаются два неприятных эффекта:

    1) Очень много головняка вываливается на сторону PO, вплоть до полной стагнации работы (даже в маленькой команде 3-5 чел). На достаточно простой технический сценарий получается куча As… I want… so that… историй настолько гранулярных, что за деревьями перестает быть виден лес. Фактически PO вынужден прорабатывать логику поведения в очень мелких деталях. Команда-то рада до соплей, потому что «перевести в тесты и дальше TDD», а вот PO и архитектор глубоко несчастны. Писать behavioral истории чтобы потом декомпозировать их в технические таски как-то уж слишком утомительно. Груминг требует дофига времени и вообще value от этой деятельности как-то не видно.

    2) Так как за behavioral историями не видно ни дизайна, ни концепций, команда начинает буферить по-страшному. Велосити в стори-поинтах прет в гору, а реальный прогресс по фичам ни к черту ни годится. На бурндауне же видится такая типовая картина: behavioral истории висят в активном состоянии несколько дней и потом закрываются «пучками», т.е. пишут по факту одну фичу на несколько историй. Что мега раздражает всех, потому как усилия на прописывание историй получается тратятся зря…

    При составлении беклога в виде технических историй такой фигни (параллельный проект) происходит меньше…

    Что я делаю не так?
    • 0
      Звучит так, как будто вы не делите истории на сценарии, а оставляете только мелкие сценарии-истории. Может быть истории слишком детальны?
      • 0
        Возможно. Фактически получается так:

        Техническая история будет выглядеть приблизительно так: «сделать справочник правил для расчета компенсаций по целям продаж для сотрудников». К этой истории будет идти описание на пол-страницы какие поля должны быть в справочнике, и как по нему считается зарплата. Понятно, она связана за всем остальным и не вполне отражает взаимодействие с пользователем. Это неприятность. Однако она описана в терминах предметного домена, понятна аналитику и т.д. Да и получается довольно компактно, дев может в нее въехать за короткое время.

        Если я пытаюсь написать то же с точки зрения BDD, я «попадаю» на описание всего этого с точки зрения актеров, т.е. «как администратор я хочу завести правило так что...» и дальше детализирую его. Но проблема в том, что этот справочник цепляет несколько «поведений» у трех разных ролей — и завести правило, и посмотреть, и выплатить и т.д. Т.е. скорее описывает процесс использования софта, нежели алгоритм его работы.

        Или есть какие-то трюки для таких ситуаций?
        • 0
          Техническая история будет выглядеть приблизительно так: «сделать справочник правил для расчета компенсаций по целям продаж для сотрудников».

          Странное сочетание — техническая история. Видимо, это не история, а задание для дева. С моей точки зрения, истории описываются со стороны тех актеров, пользователей системы.

          Но проблема в том, что этот справочник цепляет несколько «поведений» у трех разных ролей — и завести правило, и посмотреть, и выплатить и т.д.

          Хм, ну, а почему бы не использовать одно поведение в трех разных историях? Или создать одну роль объединяющую это поведение. Не понятно в чем проблема.

          Т.е. скорее описывает процесс использования софта, нежели алгоритм его работы.

          Что-то это тоже звучит странно. Здесь, видимо, какое-то смешение работы интерфейса и бизнес логики.

          И потом, видимо, BDD не стоит использовать для детального описания вообще всех требований к системе (ваш справочник со всеми полями), а стоит разделять действительные правила от бизнес политик. Это хорошо описано у Томаса и Ханта в Прагматичном программисте. Может быть здесь что-то зарыто у вас?
    • +1
      Мы столкнулись с чем-то похожим.
      Когда количество тестов по разным компонентам перевалило за тройку десятков и пошёл очередной рефакторинг, поскольку постоянно стал появляться копипаст, вышла просто адская работа. Такое ощущение, что каждая новая итерация усложняется экспоненциально.
      В итоге лично я пришёл к мнению, что BDD — не оправдывает возлагаемых на него надежд. Почему? Потому что уходит слишком много ресурсов на поддержание прослойки между Gherkin и реальным тестом, чтобы всё это оставалось в приличном виде.
      Возможно, выборочно ещё имеет смысл покрыть поведенческими тестами код, но только для случаев, когда разрабатывается такая логика системы, которую часто нужно согласовывать с бизнесом, скажем права доступа к объектам в зависимости от роли пользователя. Тогда удобно использовать листинг поведенческих тестов при очередном обсуждении.
      А когда такой необходимости — лучше использовать классические юнит-тесты.
  • +2
    «А мальчик Валентин потом это слово в словаре нашел, и так оно ему в душу запало, что стал он впоследствии главным специалистом по бихевиоризму.»
  • 0
    Стал читать, сразу наткнулся:

    CustomerLookup
    finds customer by id
    fails for duplicate customers
    Прим. «CustomerLookup [поиск заказчика]: находит заказчика по ID, не находит повторяющихся заказчиков,..»)
    Не «не находит», а падает (тест), если есть несколько одинаковых заказчиков.
    • 0
      Поправил
  • 0
    Сейчас применяю TDD в свой проекте. Проект на C++. Также есть опыт работы по TDD в Java-проекте.
    Скажу что это земля и небо. Попытаюсь пояснить свои мысли:
    1) Современные Java IDE позволяют очень быстро перейти от production к unit-test и обратно, как правило это одна горячая клавиша у меня в NetBeans это ctrl+shift+'t'
    2) В Java IDE тесты выполняются быстрее чем в C++. Потому что в С++ любая часть кода от которой зависит тестируемый production код должна быть скомпилирована. Насколько понял, а я в Java не большой знаток, там в Java не все компилируется, а только то что нужно для выполнения

    Из чего выводы:
    Чтобы прикручивать Unit-тесты в C++ проекты мне приходится дробить на более мелкие проекты, а это время! Т.е. время затрачиваемое на работу по TDD в C++ проектах значительно больше чем в Java-проектах.

    Может быть неправильно готовлю что-то? Прошу подсказать!

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.