Эволюция юнит-теста
29 Oct 2010Много слов сказано о том, как правильно писать юнит-тесты, и вообще о пользе TDD. Потом ещё и какое-то 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") );
}
}
Мы называем это типичным юнит-тестом.
Он тестирует код, но и только. Больше никаких преимуществ у него нет. Он не объясняет, почему именно такие значения.
Почему “12345678” - корректное значение? Почему “1234567” - некорректное? Кто сказал?
Именно после такого кода скептики делают вывод, что от юнит-тестов нет особой пользы.
Попытка номер №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и!