(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

В поисках качества кода: Знакомство с Behavior Driven Development (BDD) (исходники)

Эндрю Гловер

Тестирование, выполняемое разработчиком - это очевидно полезная вещь. Тестирование на ранней стадии, например, во время написания кода - еще лучше, особенно когда оно приводит к повышению качества кода. Напишите тесты заранее - и вы имеете шанс выиграть "голубую ленту" победителя регаты. Дополнительные возможности для проверки функционирования кода и его предварительной отладки, без всякого сомнения, повышают скорость разработки.

Но даже зная все это, мы еще очень далеки от того времени, когда написание тестов до написания кода станет общим стандартом. Точно так же как TDD стало следующим этапом эволюции развития экстремального программирования (eXP) и выдвинуло на первый план инфраструктуры для unit-тестирования, следующий скачок эволюции будет сделан с того уровня, где находится TDD. В этом месяце я предлагаю сделать подобный скачок в эволюции от TDD к его интуитивному родственнику: behavior-driven development (BDD) - разработке, основанной на функционировании.

Разработка, основанная на функционировании

Хотя подход с предварительным тестированием работает у многих, он подходит не для всех. На каждого разработчика приложений, с успехом применяющего TDD, найдется несколько разработчиков, активно отрицающих этот подход. Несмотря на многочисленность инфраструктур тестирования, таких как TestNG, Selenium и FEST, все равно находится много причин не выполнять тестирование кода.

Две обычные причины отказа от TDD - "у нас недостаточно времени для тестирования" и "код слишком сложный и трудно проверяемый". Другой преградой для программирования с предварительным написанием тестов является сама концепция "тест пишется до кода". Большинство рассматривает тестирование как осязаемое действие, скорее конкретное, нежели абстрактное. Опыт подсказывает, что невозможно проверить то, что еще не существует. Для некоторых разработчиков, остающихся в рамках этой концепции, идея предварительного тестирования - просто оксюморон.

Но что если вместо того, чтобы думать в терминах написания тестов и тестирования компонентов, начать думать о функциональности? Говоря про функциональность, я имею в виду как приложение должно вести себя, фактически его спецификацию.

На самом деле большинство из нас уже и так думает подобным образом. Смотрите:

Фрэнк: Что такое стек?

Линда: Это структура данных, хранящая объекты в порядке "первым вошел, последним вышел" или "последним вошел, первым вышел". Обычно у этой структуры есть API с такими методами, как push() и pop(). Иногда присутствует метод peek().

Фрэнк: Что делает метод push()?

Линда: Метод push() принимает входной объект, например, foo и помещает его во внутренний контейнер, например, массив. Метод push() обычно ничего не возвращает.

Фрэнк: Что будет, если передать методу push() два объекта, например, сначала foo, а потом bar?

Линда: Второй объект bar должен оказаться наверху концептуального стека, содержащего по крайней мере два объекта, так что при вызове метода pop() объект bar должен быть извлечен первым, до первого объекта foo. Если метод pop() вызвать еще раз, должен быть возвращен объект foo и стек должен стать пустым (предполагается, что в нем ничего не было до того, как мы добавили эти два объекта).

Фрэнк: Так метод pop() удаляет самый последний элемент, добавленный в стек?

Линда: Да, метод pop() должен удалить верхний элемент, при этом предполагается, что в стеке есть элементы, чтобы их удалять. Метод peek() работает точно также, но при этом объект не удаляется. Метод peek() должен оставить верхний элемент в стеке.

Фрэнк: Что будет, если вызвать метод pop(), когда в стек еще ничего не было добавлено?

Линда: Метод pop() должен выдать исключение, показывающее, что в стек еще ничего не добавлялось.

Фрэнк: Что будет, если выполнить команду push() null?

Линда: Стек должен выдать исключение, так как null не является допустимым значением для метода push().

Можно ли выделить что-нибудь особенное в этом диалоге, кроме того, что Фрэнк не силен в структурах данных? Нигде не использовалось слово "тестирование". Однако слово "должен" проскальзывало регулярно и звучало довольно естественно.

Действовать естественно

 
Какие инфраструктуры можно использовать?

Аннотации позволяют реализовать BDD в JUnit и TestNG. Но на мой взгляд, гораздо интереснее использовать специальные BDD-инфраструктуры, такие как JBehave. Эта инфраструктура предоставляет возможности для определения поведения класса, - инфраструктуру для обработки ожидаемых результатов , которая поддерживает более "литературный" стиль программирования.

В подходе BDD нет ничего нового или революционного. Это просто эволюционное ответвление подхода TDD, где слово "тест" заменено словом "должен". Если отложить в сторону слова, то многие найдут понятие "должен" более естественным для процесса разработки, чем понятие "тест". Мышление в терминах функциональности (того, что код должен делать), приводит к подходу, когда сначала пишутся классы для проверки спецификации, которые, в свою очередь, могут оказаться очень эффективным инструментом реализации.

Используя разговор между Фрэнком и Линдой в качестве основы, давайте посмотрим, как подход BDD поддерживает разработку в том стиле, который намеревались популяризовать с помощью TDD.


JBehave

JBehave - это BDD-инфраструктура для платформы Java, основанная на принципах xUnit. Как естественно предположить, в JBehave делается упор на слово должен , а не на тест . Как и в случае JUnit, классы JBehave можно запускать в вашей стандартной среде разработки или на предпочитаемой вами платформе для сборки проекта, такой как Ant.

JBehave позволяет создавать классы для проверки функциональности, почти так же как и в JUnit; однако в случае с JBehave нет необходимости наследовать от какому-либо конкретному базовому классу, и все методы для проверки должны начинаться с should, а не с test, как показано в листинге 1.

Листинг 1. Простой класс для проверки функциональности стека

                
public class StackBehavior {
 public void shouldThrowExceptionUponNullPush() throws Exception{}
 public void shouldThrowExceptionUponPopWithoutPush() throws Exception{}
 public void shouldPopPushedValue() throws Exception{}
 public void shouldPopSecondPushedValueFirst() throws Exception{}
 public void shouldLeaveValueOnStackAfterPeep() throws Exception{}
}

Все методы, определенные в листинге 1, начинаются с should , и их названия - это вполне читабельные и понятные предложения. Получившийся класс StackBehavior описывает многие функциональные возможности стека из разговора между Фрэнком и Линдой.

Например, Линда упомянула, что стек должен выдавать исключение, если пользователь попробует поместить в него null. Посмотрите на первый метод класса StackBehavior: он так и называется shouldThrowExceptionUponNullPush(). Другие методы называются по этому же шаблону. Такая описательная манера наименования методов, естественно, не являющаяся привилегией JBehave или BDD, позволяет определить неудачно работающую функциональность так, чтобы ее было легко прочесть и понять, как будет показано позже.

Возвращаясь к методу shouldThrowExceptionUponNullPush(), как можно проверить эту функциональность? Кажется разумным сначала добавить метод push() в класс Stack, что довольно просто.

Листинг 2. Простое определение стека для облегчения проверки требуемой функциональности

                
public class Stack<E> {
 public void push(E value) {}
}

Можно заметить, что здесь приведено минимальное количество кода для стека, позволяющее начать проверку требуемой функциональности. Как упоминала Линда, эта функциональность крайне проста: если кто-то вызовет метод push() со значением null, стек должен выдать исключение. Как я реализовал это поведение, можно увидеть в листинге 3.

Листинг 3. Стек должен выдать исключение, если в него добавляется null

                
public void shouldThrowExceptionUponNullPush() throws Exception{
 final Stack<String> stStack = new Stack<String>();

 Ensure.throwsException(RuntimeException.class, new Block(){
   public void run() throws Exception {
    stStack.push(null);
   }
 });
}

Большие надежды и переопределения

В листинге 3 есть несколько уникальных элементов JBehave, которые необходимо пояснить. Сначала мы создаем экземпляр класса Stack, ограниченный типами String с помощью Java 5 generics. Затем с помощью инфраструктуры ожиданий JBehave естественным способом моделируется желаемое поведение. Класс Ensure аналогичен классу Assert в JUnit или TestNG; однако в нем добавлен набор методов, которые помогают сделать API более читаемым (часто это называется литературное программирование ). В листинге 3 я сделал так, чтобы при вызове метода push() с параметром null выдавалось исключение RuntimeException.

В JBehave также введен тип Block, который реализуется за счет переопределения метода run() методом, содержащим поведение, которое нужно проверить. Внутри JBehave проверяет, что желаемое исключение не было выдано, а, следовательно, и не обработано, и генерирует отчет о неудачном запуске. Похожий подход к переопределению стандартных классов уже встречался в прошлой статье Unit testing Ajax with the Google Web Toolkit; в том случае переопределялся класс Timer из GWT.

Если сейчас запустить код для проверки функциональности из листинга 3, то будет зафиксирована неудача запуска. В том состоянии, как сейчас закодирован метод push(), он ничего не делает, поэтому исключение выдано не будет, как можно увидеть из результатов работы, приведенных в листинге 4.

Листинг 4. Желаемое поведение не подтвердилось

                
1) StackBehavior should throw exception upon null push:
VerificationException: Expected: 
object not null
but got: 
null:

Предложение "StackBehavior should throw exception upon null push" в листинге 4 соответствует названию метода для проверки поведения - shouldThrowExceptionUponNullPush() и названию класса. Естественно, JBehave отчитывается, что при запуске данного кода для проверки функциональности ничего не произошло. Потому следующим шагом я сделаю так, чтобы эта функциональность срабатывала - для этого добавим проверку на null в методе 5.

Листинг 5. Добавление нужной функциональности в класс стека

                
public void push(E value) {
  if(value == null){
   throw new RuntimeException("Can't push null");
  }
}

Теперь, если перезапустить код для проверки поведения стека, все сработает нормально, как показано в листинге 6.

Листинг 6. Успешное выполнение

                
Time: 0.021s

Total: 1. Success!

Поведение управляет разработкой

Неправда ли, результат работы в листинге 6 похож на результат работы JUnit? Вероятно, это не случайно. Как упоминалось, JBehave построен на основе парадигмы xUnit и даже поддерживает фикстуры с помощью методов setUp() и tearDown(). Поскольку я, возможно, буду использовать экземпляр класса Stack в своем классе для проверки поведения, я мог бы поместить эту логику в фикстуру, как показано в листинге 7. Отметим, что JBehave следует тем же правилам при работе с фикстурами, что и JUnit, запуская методы setUp() и tearDown() для каждого метода для проверки функциональности.

Листинг 7. Фикстуры в JBehave

                
public class StackBehavior {
 private Stack<String> stStack;
  
 public void setUp() {
  this.stStack = new Stack<String>();
 }
 //...
}

Перейдем к следующему методу для проверки поведения shouldThrowExceptionUponPopWithoutPush(). Он проверяет функциональность, аналогичную той, что проверял метод shouldThrowExceptionUponNullPush() из листинга. Как видно из листинга 8, в этом нет ничего сложного, не так ли?

Листинг 8. Проверка извлечения элементов из пустого стека

                
public void shouldThrowExceptionUponPopWithoutPush() throws Exception{
		
 Ensure.throwsException(RuntimeException.class, new Block() {
   public void run() throws Exception {
    stStack.pop();
   }
 });
}

Как можно догадаться, листинг 8 на данный момент не может быть скомпилирован, так как метод pop() еще не написан. Однако, прежде чем начать писать метод pop(), необходимо рассмотреть несколько моментов.

Реализация функциональности

Технически я мог бы сейчас реализовать метод pop() так, чтобы он просто выдавал исключение всегда, вне зависимости от порядка вызовов. Но для дальнейшей реализации требуемой функциональности более эффективен подход, соответствующий желаемой спецификации. В этом случае для того, чтобы метод pop() выдавал исключение, если до этого не был вызван метод push(), или, логически, если в стеке нет элементов, необходимо, чтобы стек имел состояние. Как упоминала раньше Линда, у стека есть "внутренний контейнер", который физически содержит элементы стека. Поэтому можно создать в классе Stack объект ArrayList для хранения значений, переданных через метод push(), как показано в листинге 9.

Листинг 9. Стеку необходим какой-нибудь внутренний способ хранения объектов

                
public class Stack<E> {
 private ArrayList<E> list; 

 public Stack() {
  this.list = new ArrayList<E>();
 }
 //...
}

Теперь можно запрограммировать поведение метода pop(), которое гарантирует выдачу исключения, если в стеке нет элементов.

Листинг 10. Реализовать метод pop() стало проще

                
public E pop() {
 if(this.list.size() > 0){
  return null;
 }else{
  throw new RuntimeException("nothing to pop");
 }
}

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

Следующий метод shouldPopPushedValue() для проверки функциональности также определяется достаточно просто. Необходимо просто вызвать метод push() с параметром ("test") и проверить, что при вызове pop() возвращается это же значение.

Листинг 11. Если значение удалось добавить, то его должно быть можно и извлечь

                
public void shouldPopPushedValue() throws Exception{
 stStack.push("test");
 Ensure.that(stStack.pop(), m.is("test"));
}

Поиск совпадений

 
Класс UsingMatchers

Можно заметить, что код в листинге 12 выглядит не очень элегантно. Переменная m в листинге 11 отрицательно влияет на читаемость кода ("проверим, что извлеченное значение m ( что это? ) равно test"). Этого можно избежать, используя класс UsingMatchers и расширив специальный базовый класс UsingMiniMock, предоставляемые JBehave. В этом подходе последняя строка в листинге 11 будет выглядеть более читабельно: Ensure.that(stStack.pop(), is("test")).

В листинге 11 проверяется, что метод pop() возвращает значение "test". При использовании класса Ensure из JBehave часто оказывается, что необходим более функциональный способ для определения ожидаемых значений. Для решения этой проблемы JBehave предлагает класс Matcher для реализации сложных ожидаемых значений. В нашем случае мы воспользуемся классом UsingMatchers из набора JBehave (переменная m в листинге 11), чтобы получить доступ к таким методам, как is(), and(), or() и другим сложным способfм для построения ожидаемых значений в более "литературном" стиле.

Переменная m в листинге 11 - это статический элемент класса StackBehavior, как показано в листинге 12.

Листинг 12. Использование класса UsingMatchers в классе для проверки функциональности

                
private static final UsingMatchers m = new UsingMatchers(){};

Теперь можно запустить новый метод для проверки поведения из листинга 11, но при этом происходит ошибка, как показано в листинге 13.

Листинг 13. Новый метод для проверки поведения не работает

                
Failures: 1.

1) StackBehavior should pop pushed value:
java.lang.RuntimeException: nothing to pop

В чем же проблема? Дело в том, что метод push() реализован не до конца. Если помните, в листинге 5, я ограничился минимальной реализацией, чтобы запустить код для проверки функциональности. Теперь пришло время закончить работу и действительно добавлять принимаемые значения во внутренний контейнер (если эти значения не null). Реализация этого метода приведена в листинге 14.

Листинг 14. Реализация метода push

                
public void push(E value) {
 if(value == null){
  throw new RuntimeException("Can't push null");
 }else{
  this.list.add(value);
 }
}

Однако если запустить код для проверки этого поведения, все равно происходит ошибка!

Листинг 15. JBehave сообщает о значении null вместо того, чтобы выдавать исключение

                
1) StackBehavior should pop pushed value:
VerificationException: Expected: 
same instance as <test>
but got: 
null:

По крайней мере, ошибка из листинга 15 отличается от ошибки из листинга 13. В данном случае произошло не исключение, а не было обнаружено ожидаемое значение "test": вместо этого из стека был возвращен null. Если посмотреть на листинг 10, станет понятна причина: метод pop() изначально был закодирован так, чтобы возвращать null, если во внутреннем контейнере есть элементы. Эту проблему достаточно просто исправить.

Листинг 16. Окончательная реализация метода pop

                
public E pop() {
 if(this.list.size() > 0){
  return this.list.remove(this.list.size());
 }else{
  throw new RuntimeException("nothing to pop");
 }
}

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

Листинг 17. Опять ошибка

                
1) StackBehavior should pop pushed value:
java.lang.IndexOutOfBoundsException: Index: 1, Size: 1

После изучения листинга 17 проблема становится понятной: при работе ArrayList необходимо вести отсчет элементов с 0 .

Листинг 18. Решение проблемы: начинаем отсчет элементов с 0

                
public E pop() {
 if(this.list.size() > 0){
  return this.list.remove(this.list.size()-1);
 }else{
  throw new RuntimeException("Nothing to pop");
 }
}

Функциональность стека

Сейчас методы push() и pop() реализованы так, что бы некоторые методы для проверки поведения могли успешно выполниться. Однако еще не реализована основная функциональность стека, например, логика, связанная с множественными вызовами методов push() и pop(), вместе с незапланированным вызовом метода peek().

Сначала необходимо убедиться, что выполняется базовый алгоритм стека (первым вошел, последним вышел) с помощью метода shouldPopSecondPushedValueFirst() для проверки функциональности.

Листинг 19. Проверка стандартного поведения стека

                
public void shouldPopSecondPushedValueFirst() throws Exception{
 stStack.push("test 1");
 stStack.push("test 2");
 Ensure.that(stStack.pop(), m.is("test 2"));
}

Код в листинге 19 работает, как и запланировано, так что можно реализовать другой метод для проверки поведения из листинга 20, чтобы проверить, что двойной вызов метода pop() работает правильно.

Листинг 20. Дополнительные проверки функциональности стека

                
public void shouldPopValuesInReverseOrder() throws Exception{
 stStack.push("test 1");
 stStack.push("test 2");
 Ensure.that(stStack.pop(), m.is("test 2"));
 Ensure.that(stStack.pop(), m.is("test 1"));
}

Далее необходимо убедиться, что метод peek() работает, как запланировано. По словам Линды, метод peek() работает точно так же, как и метод pop(), но "должен оставлять верхний элемент в стеке". В соответствии с этим в листинге 21 подготовлен метод shouldLeaveValueOnStackAfterPeep() для проверки подобного поведения.

Листинг 21. Проверка того, что метод peek() оставляет верхний элемент в стеке

                
public void shouldLeaveValueOnStackAfterPeep() throws Exception{
 stStack.push("test 1");
 stStack.push("test 2");
 Ensure.that(stStack.peek(), m.is("test 2"));
 Ensure.that(stStack.pop(), m.is("test 2"));
}

Так как метод peek() еще не определен, то код в листинге 21 не скомпилируется. В листинге 22 определен "скелет" реализации метода peek().

Листинг 22. Определение метода peek()

                
public E peek() {
 return null;
}

Теперь класс StackBehavior компилируется, но по-прежнему не работает.

Листинг 23. Не работает - возвращается null

                
1) StackBehavior should leave value on stack after peep:
VerificationException: Expected: 
same instance as <test 2>
but got: 
null:

Логически метод peek() не удаляет элемент из внутренней коллекции, а просто передает ссылку на него. Следовательно, нужно вызвать метод get() на объекте ArrayList, а не remove(), как показано в листинге 24.

Листинг 24. Возвращаем, но не удаляем!

                
public E peek() {
 return this.list.get(this.list.size()-1);
}

Дополнительные проверки поведения стека

Теперь, если перезапустить код из листинга 21, выводится отчет об успешном прохождении теста. Однако в этом упражнении остался один нераскрытый аспект: как должен вести себя метод peek(), если в стеке ничего нет? Метод pop() в этом случае должен выдать исключение, должен ли метод peek() поступить так же?

Линда про это ничего не говорила, так что придется реализовать проверку подобного поведения самостоятельно. В листинге 25 приведен код для сценария "что произойдет, если вызвать метод peek() без предварительного вызова метода push()?"

Листинг 25. Что произойдет если вызвать peek() без предварительного вызова push()?

                
public void shouldReturnNullOnPeekWithoutPush() throws Exception{
 Ensure.that(stStack.peek(), m.is(null));
}

Снова ничего удивительного. Запуск кода зафиксировал ошибку, как показано в листинге 26.

Листинг 26. Методу peek() нечего возвращать

                
1) StackBehavior should return null on peek without push:
java.lang.ArrayIndexOutOfBoundsException: -1

Чтобы устранить этот дефект, подойдет логика, сходная с логикой в методе pop(), как показано в листинге 27.

Листинг 27. Необходимые исправления для метода peek()

                
public E peek() {
 if(this.list.size() > 0){
  return this.list.get(this.list.size()-1);
 }else{
  return null;
 }
}

После всех модификаций и исправлений код класса Stack выглядит, как показано в листинге 28.

Листинг 28. Работающий стек

                
import java.util.ArrayList;

public class Stack<E> {

 private ArrayList<E> list;

 public Stack() {
  this.list = new ArrayList<E>();
 }

 public void push(E value) {
  if(value == null){
   throw new RuntimeException("Can't push null");
  }else{
   this.list.add(value);
  }
 }

 public E pop() {
  if(this.list.size() > 0){
   return this.list.remove(this.list.size()-1);
  }else{
   throw new RuntimeException("Nothing to pop");
  }
 }

 public E peek() {
  if(this.list.size() > 0){
   return this.list.get(this.list.size()-1);
  }else{
   return null;
  }
 }
}

К этому моменту класс StackBehavior обладает семью методами для проверки поведения, гарантирующими, что класс Stack работает согласно спецификации Линды и моим дополнениям. Возможно, классу Stack требуется некоторый рефакторинг, например, метод pop() должен для проверки вызывать метод peek(), а не метод size(), но благодаря процессу, ориентированному на спецификацию, у меня имеется инфраструктура, чтобы вносить изменения с определенной долей уверенности. Если что-нибудь будет нарушено, об этом тут же станет известно.

Заключение

Что можно заметить из этой статьи, посвященной исследованию BDD, - это то, что Линда - это на самом деле клиент. Соответственно можно говорить, что Фрэнк - это разработчик. Если отвлечься от конкретной предметной области (структуры данных) и заменить эту область другой, например, приложением для call-центра, суть процесса останется той же. Линда - клиент или эксперт в предметной области - говорит, что именно система, функция или приложение должны делать, а кто-нибудь вроде Фрэнка использует BDD для проверки, что он правильно услышал и реализовал требования клиента.

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

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



 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 30.05.2008 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Microsoft Office 365 для Дома 32-bit/x64. 5 ПК/Mac + 5 Планшетов + 5 Телефонов. Подписка на 1 год.
GFI FaxMaker и 1 год поддержки (10-49 лицензий)
VMware Horizon 7 Standard : 10 Pack (CCU)
ABBYY Lingvo x6 Европейская Профессиональная версия, электронный ключ
FastReport.Desktop
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Программирование на Microsoft Access
CASE-технологии
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
Программирование в AutoCAD
СУБД Oracle "с нуля"
Проект mic-hard - все об XP - новости, статьи, советы
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100