Много слов сказано о том, как правильно писать юнит-тесты, и вообще о пользе TDD. Потом ещё и какое-то BDD замаячило на горизонте. Приходится разбираться, что из них лучше и между ними какая разница. Может, это и есть причина, почему большинство разработчиков решили не заморачиваться и до сих пор не используют ни того, ни другого?
Коротко: BDD — это дальнейшее развитие идей TDD, стало быть, его и надо использовать. А разницу между TDD и BDD я попробую объяснить на простом примере.
Рассмотрим 3 ревизии одного юнит-теста, который я нашёл в одном реальном проекте.
Первая версия этого юнит-теста была такой:
Мы называем это типичным юнит-тестом. Он тестирует код, но и только. Больше никаких преимуществ у него нет. Именно после такого кода скептики делают вывод, что от юнит-тестов нет особой пользы.
В какой-то момент пришёл разработчик и решил применить к этому коду некоторые "best practices" из TDD: разбить тест-метод на несколько маленьких, так чтобы каждый из них тестировал только одну вещь, и дать им соответствующие имена.
Вот что у него получилось:
Мы называем это хорошим юнит-тестом. Он гораздо легче читается: по названиям переменных легко догадаться, что 13 символов — это слишком много, 7 — слишком мало, а 8 символов — это нормально.
Спустя какое-то время приходит ещё один разработчик и замечает, что даже этот хороший юнит-тест не является вполне читабельным и не предоставляет достаточно информации о том, как работает класс ReferenceNumber. Его можно понять, но для этого всё-таки надо залезть в код и немножко подумать.
Разработчик продолжает процесс разбивки и переименования:
Мы называем это спецификацией в стиле BDD. Названия методов говорят почти на человеческом языке о том, как должен работать код. Мысленно вставив перед заглавными буквами пробелы, мы получаем спецификацию кода на английском языке. Чтобы понять, как работает класс, мы не должны залезать в код — достаточно прочитать называния. А если в ходе изменения кода в него внесли ошибку, и юнит-тест сломался, мы по названию сломавшегося тест-метода наверняка сможем определить, что за ошибка допущена в коде.
Можно сказать, третья версия отличается от предыдущих тем, что она описывает поведение класса. Это достигается за счёт использования таких слов как «should» и «contain»: «мой класс должен вести себя так-то и так-то», «мой метод должен делать то-то и то-то».
Так вот, идея BDD как раз и заключается в том, чтобы вместо слов «test» и «assert» использовать слова «spec» и «should». Да-да, разница всего лишь в словах, но именно это, по замыслу авторов BDD, и делает спецификации удобочитаемыми, а написаниетестов спецификаций до кода — естественным для человеческого мозга.
Убедиться в этом вы можете, взглянув на тот же пример, переведённый с языка JUnit на язык Easyb:
Отчёт о запуске этихтестов спецификаций фактически может служить документацией:

Осталось добавить, что библиотеки для написания BDD спецификаций есть и для других языков: Java (JDave, JBehave), Ruby (RSpec, RBehave, Cucumber), Groovy (Easyb), Scala (Scala-test), PHP (Behat), CPP (CppSpec), .Net (SpecFlow, Shouldly), Python (Lettuce, Cucumber).
А если по независящим от вас причинам вы не можете пересесть с JUnit на что-то другое — тоже ничего, только помните о третьем примере. Кстати, в этом случае вам пригодится библиотека Harmcrest.
Как завещал Козьма Прутков: товарищ, BDDи!
Коротко: BDD — это дальнейшее развитие идей TDD, стало быть, его и надо использовать. А разницу между TDD и BDD я попробую объяснить на простом примере.
Рассмотрим 3 ревизии одного юнит-теста, который я нашёл в одном реальном проекте.
Попытка номер №1
Первая версия этого юнит-теста была такой:
public class ReferenceNumberTest { @Test public void testValidate() { assertFalse( ReferenceNumber.validate("1234567890123") ); assertFalse( ReferenceNumber.validate("1234567") ); assertTrue( ReferenceNumber.validate("12345678") ); } }
Мы называем это типичным юнит-тестом. Он тестирует код, но и только. Больше никаких преимуществ у него нет. Именно после такого кода скептики делают вывод, что от юнит-тестов нет особой пользы.
Попытка номер №2
В какой-то момент пришёл разработчик и решил применить к этому коду некоторые "best practices" из TDD: разбить тест-метод на несколько маленьких, так чтобы каждый из них тестировал только одну вещь, и дать им соответствующие имена.
Вот что у него получилось:
public class ReferenceNumberTest { @Test public void testTooLong() { String len13 = "1234567891111"; assertEquals(len13.length(), 13); assertEquals(ReferenceNumber.validate(len13), false); } @Test public void testTooShort() { String len7 = "1234567"; assertEquals(len7.length(), 7); assertEquals(ReferenceNumber.validate(len7), false); } @Test public void testOk() { String len8 = "12345678"; assertEquals(len8.length(), 8); assertEquals(ReferenceNumber.validate(len8), true); String len12 = "123456789111"; assertEquals(len12.length(), 12); assertEquals(ReferenceNumber.validate(len12), true); } }
Мы называем это хорошим юнит-тестом. Он гораздо легче читается: по названиям переменных легко догадаться, что 13 символов — это слишком много, 7 — слишком мало, а 8 символов — это нормально.
Попытка номер №3
Спустя какое-то время приходит ещё один разработчик и замечает, что даже этот хороший юнит-тест не является вполне читабельным и не предоставляет достаточно информации о том, как работает класс ReferenceNumber. Его можно понять, но для этого всё-таки надо залезть в код и немножко подумать.
Разработчик продолжает процесс разбивки и переименования:
public class ReferenceNumberTest { @Test public void nullIsNotValidReferenceNumber() { assertFalse(ReferenceNumber.validate(null)); } @Test public void referenceNumberShouldBeShorterThan13() { assertFalse(ReferenceNumber.validate("1234567890123")); } @Test public void referenceNumberShouldBeLongerThan7() { assertFalse(ReferenceNumber.validate("1234567")); } @Test public void referenceNumberShouldContainOnlyNumbers() { assertFalse(ReferenceNumber.validate("1234567ab")); assertFalse(ReferenceNumber.validate("abcdefghi")); assertFalse(ReferenceNumber.validate("---------")); assertFalse(ReferenceNumber.validate(" ")); } @Test public void validReferenceNumberExamples() { assertTrue(ReferenceNumber.validate("12345678")); assertTrue(ReferenceNumber.validate("123456789")); assertTrue(ReferenceNumber.validate("1234567890")); assertTrue(ReferenceNumber.validate("12345678901")); assertTrue(ReferenceNumber.validate("123456789012")); } }
Мы называем это спецификацией в стиле BDD. Названия методов говорят почти на человеческом языке о том, как должен работать код. Мысленно вставив перед заглавными буквами пробелы, мы получаем спецификацию кода на английском языке. Чтобы понять, как работает класс, мы не должны залезать в код — достаточно прочитать называния. А если в ходе изменения кода в него внесли ошибку, и юнит-тест сломался, мы по названию сломавшегося тест-метода наверняка сможем определить, что за ошибка допущена в коде.
Между прочим, этот пример эволюции юнит-теста я собирался показать на семинаре devclub.eu по BDD в Таллине. И вот, за день до семинара я обнаружил, что я забыл скопировать исходный код самого класса ReferenceNumber, который мы тут всю дорогу тестируем. Что делать? Паника! До семинара остался один день! Мне нужно было срочно самому написать его заново.
А теперь посмотрите на эти три тест-класса и подумайте, какой из них помог мне восстановить логику класса ReferenceNumber.
И наконец, BDD
Можно сказать, третья версия отличается от предыдущих тем, что она описывает поведение класса. Это достигается за счёт использования таких слов как «should» и «contain»: «мой класс должен вести себя так-то и так-то», «мой метод должен делать то-то и то-то».
Так вот, идея BDD как раз и заключается в том, чтобы вместо слов «test» и «assert» использовать слова «spec» и «should». Да-да, разница всего лишь в словах, но именно это, по замыслу авторов BDD, и делает спецификации удобочитаемыми, а написание
Убедиться в этом вы можете, взглянув на тот же пример, переведённый с языка JUnit на язык Easyb:
description "ReferenceNumber" it "should not be null", { ReferenceNumber.validate(null).shouldBe false } it "should be shorter than 13", { ReferenceNumber.validate("1234567890123").shouldBe false } it "should be longer than 7", { ReferenceNumber.validate("1234567").shouldBe false } it "should contain only numbers", { ReferenceNumber.validate("1234567ab").shouldBe false ReferenceNumber.validate("abcdefghi").shouldBe false ReferenceNumber.validate("---------").shouldBe false ReferenceNumber.validate(" ").shouldBe false } it "valid reference number examples", { ReferenceNumber.validate("12345678").shouldBe true ReferenceNumber.validate("123456789").shouldBe true ReferenceNumber.validate("1234567890").shouldBe true ReferenceNumber.validate("12345678901").shouldBe true ReferenceNumber.validate("123456789012").shouldBe true }
Отчёт о запуске этих

Кроме it и should, в BDD есть и другие важные слова, такие как given, when и then, а также before и after, ну и вдобавок ensure, narrative и «should behave as». Также BDD подходит не только для юнит-тестов, но и для функциональных/интеграционных тестов, но это уже выходит за рамки данной статьи. Сейчас нас интересует уровень юнит-тестов. Цель данной статьи — показать, что их можно писать по-разному.
Осталось добавить, что библиотеки для написания BDD спецификаций есть и для других языков: Java (JDave, JBehave), Ruby (RSpec, RBehave, Cucumber), Groovy (Easyb), Scala (Scala-test), PHP (Behat), CPP (CppSpec), .Net (SpecFlow, Shouldly), Python (Lettuce, Cucumber).
А если по независящим от вас причинам вы не можете пересесть с JUnit на что-то другое — тоже ничего, только помните о третьем примере. Кстати, в этом случае вам пригодится библиотека Harmcrest.
Как завещал Козьма Прутков: товарищ, BDDи!
