Изящный, гибкий и понятный код, который легко модифицировать, который корректно работает и который не подкидывает своим создателям неприятных сюрпризов. Неужели, подобное возможно? Чтобы достичь цели, попробуйте тестировать программу еще до того, как она написана. Именно такая парадоксальная идея положена в основу методики TDD (Test-Driven-Development — разработка, основанная на тестировании). Бессмыслица? Не спешите делать скороспелых выводов. Рассматривая применение TDD на примере разработки реального программного кода, автор демонстрирует простоту и мощь этой новой методики. В книге рассматриваются два программных проекта, целиком и полностью реализованных с использованием TDD. За рассмотрением примеров следует обширный каталог приемов работы в стиле TDD, а также паттернов и рефакторингов, имеющих отношение к TDD. Книга будет полезна для любого программиста, желающего повысить производительность свой работы и получить удовольствие от программирования.
Содержание
Предисловие
Храбрость
Благодарности
От издательства
Введение
Часть I. На примере денег
Глава 1. Мультивалютные деньги
Глава 2. Вырождающиеся объекты
Глава 3. Равенство для всех
Глава 4. Данные должны быть закрытыми
Глава 5. Поговорим о франках
Глава 6. Равенство для всех, вторая серия
Глава 7. Яблоки и апельсины
Глава 8. Создание объектов
Глава 9. Потребность в валюте
Глава 10. Избавление от двух разных версий times()
Глава 11. Корень всего зла
Глава 12. Сложение, наконец-то
Глава 13. Делаем реализацию реальной
Глава 14. Обмен валюты
Глава 15. Смешение валют
Глава 16. Абстракция, наконец-то!
Глава 17. Ретроспектива денежного примера
Что дальше?
Метафора
Использование JUnit
Метрики кода
Процесс
Качество тестов
Последний взгляд назад
Часть II. На примере xUnit
Глава 18. Первые шаги на пути к xUnit
Глава 19. Сервируем стол (метод setUp)
Глава 20. Убираем со стола (метод tearDown)
Глава 21. Учет и контроль
Глава 22. Обработка несработавшего теста
Глава 23. Оформляем тесты в набор
Глава 24. Ретроспектива xUnit
Часть III. Паттерны для разработки через тестирование
Глава 25. Паттерны разработки, основанной на тестах
Тест
Изолированный тест (Isolated Test)
Список тестов (Test List)
Вначале тест (Test First)
Вначале оператор assert (Assert First)
Тестовые данные (Test Data)
Понятные данные (Evident Data)
Глава 26. Паттерны красной полосы
One Step Test (Тест одного шага)
Starter Test (Начальный тест)
Explanation Test (Объясняющий тест)
Learning Test (Тест для изучения)
Another Test (Еще один тест)
Regression Test (Регрессионный тест)
Break (Перерыв)
Do over (Начать сначала)
Cheap Desk, Nice Chair (Дешевый стол, хорошие кресла)
Глава 27. Паттерны тестирования
Дочерний тест (Child Test)
Mock Object (Поддельный объект)
Self Shunt (Самошунтирование)
Log String (Строка-журнал)
Crash Test Dummy (Тестирование обработки ошибок)
Broken Test (Сломанный тест)
Clean Check-in (Чистый выпускаемый код)
Глава 28. Паттерны зеленой полосы
Fake It (Подделка)
Triangulate (Триангуляция)
Obvious Implementation (Очевидная реализация)
One to Many (От одного ко многим)
Глава 29. Паттерны xUnit
Assertion
Fixture (Фикстура)
External Fixture (Внешняя фикстура)
Test Method (Тестовый метод)
Exception Test (Тест исключения)
All Tests (Все тесты)
Глава 30. Паттерны проектирования
Command (Команда)
Value Object (Объект-значение)
Null Object (Нуль-объект)
Template Method (Шаблонный метод)
Pluggable Object (Встраиваемый объект)
Pluggable Selector (Встраиваемый переключатель)
Factory Method (Фабричный метод)
Imposter (Самозванец)
Composite (Компоновщик)
Collecting Parameter (Накопление в параметре)
Singleton (Одиночка)
Глава 31. Рефакторинг
Reconcile Differences (Согласование различий)
Isolate Change (Изоляция изменений)
Migrate Data (Миграция данных)
Extract Method (Выделение метода)
Inline Method (Встраивание метода)
Extract Interface (Выделение интерфейса)
Move Method (Перемещение метода)
Method Object (Метод в объект)
Add Parameter (Добавление параметра)
Method Parameter to Constructor Parameter (Параметр метода в параметр конструктора)
Глава 32. Развитие навыков TDD
Насколько большими должны быть шаги?
Что не подлежит тестированию?
Как определить качество тестов?
Каким образом TDD ведет к созданию инфраструктур?
Сколько должно быть тестов?
Когда следует удалять тесты?
Каким образом язык программирования и среда разработки влияют на TDD?
Можно ли использовать TDD для разработки крупномасштабных систем?
Можно ли осуществлять разработку приложения исходя из тестов уровня приложения?
Как можно перейти к использованию TDD в середине работы над проектом?
Для кого предназначена методика TDD?
Зависит ли эффективность TDD от начальных условий?
Каким образом методика TDD связана с паттернами?
Почему TDD работает?
Что означает имя?
Как методика TDD связана с практиками экстремального программирования?
Нерешенные проблемы TDD
Послесловие
Приложение I. Диаграммы взаимовлияния
Обратная связь
Контроль над системой
Приложение II. Фибоначчи
Алфавитный указатель
ОТРЫВОК
Обмен валюты
$5 + 10 CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Операция $5 + $5 возвращает объект Money
Bank.reduce(Money)
Приведение объекта Money с одновременной конверсией валют
Reduce(Bank,String)
Изменения, перемены, обмены - их объятия заслуживают внимания (особенно если у вас есть книга с фразой в заголовке "в объятиях изменений" (embrace change)). Впрочем, нас заботит простейшая форма обмена - у нас есть два франка и мы хотим получить один доллар. Это звучит как готовый тест:
public void testReduceMoneyDifferentCurrency() {
Bank bank= new Bank();
bank.addRate("CHF", "USD", 2);
Money result= bank.reduce(Money.franc(2), "USD");
assertEquals(Money.dollar(1), result);
}
Когда я конвертирую франки в доллары, я просто делю значение на два (мы по-прежнему игнорируем все эти неприятные проблемы, связанные с дробными числами). Чтобы сделать полоску зеленой, мы добавляем в код еще одну уродливую конструкцию:
Money
public Money reduce(String to) {
int rate = (currency.equals("CHF") && to.equals("USD"))
? 2
: 1;
return new Money(amount / rate, to);
}
Получается, что класс Money знает о курсе обмена. Это неправильно. Единственным местом, в котором выполняются любые операции, связанные с курсом обмена, должен быть класс Bank. Мы должны передать параметр типа Bank в метод Expression.reduce(). (Вот видите? Мы так и думали, что нам это потребуется. И мы оказались правы.) Вначале меняем вызывающий код:
Bank
Money reduce(Expression source, String to) {
return source.reduce(this, to);
}
Затем меняем код реализаций:
Expression
Money reduce(Bank bank, String to);
Sum
public Money reduce(Bank bank, String to) {
int amount= augend.amount + addend.amount;
return new Money(amount, to);
}
Money
public Money reduce(Bank bank, String to) {
int rate = (currency.equals("CHF") && to.equals("USD"))
? 2
: 1;
return new Money(amount / rate, to);
}
Методы должны быть открытыми (public), так как все методы интерфейсов должны быть открытыми (я надеюсь, можно не объяснять, почему).
Теперь мы можем вычислить курс обмена внутри класса Bank:
Bank
int rate(String from, String to) {
return (from.equals("CHF") && to.equals("USD"))
? 2
: 1;
}
И обратиться к объекту bank с просьбой предоставить значение курса обмена:
Money
public Money reduce(Bank bank, String to) {
int rate = bank.rate(currency, to);
return new Money(amount / rate, to);
}
Эта надоедливая цифра 2 снова отсвечивает как в разрабатываемом коде, так и в теле теста. Чтобы избавиться от нее, мы должны создать таблицу обменных курсов в классе Bank и при необходимости обращаться к этой таблице для получения значения обменного курса. Для этой цели мы могли бы воспользоваться хэш-таблицей, которая ставит в соответствие паре валют соответствующий обменный курс. Можем ли мы в качестве ключа использовать двухэлементный массив, содержащий в себе две валюты? Проверяет ли метод Array.equals() эквивалентность элементов массива?
public void testArrayEquals() {
assertEquals(new Object[] {"abc"}, new Object[] {"abc"});
}
Нет. Тест не сработал. Придется создавать специальный объект, который будет использоваться в качестве ключа хэш-таблицы:
Pair
private class Pair {
private String from;
private String to;
Pair(String from, String to) {
this.from= from;
this.to= to;
}
}
Мы планируем использовать объекты Pair в качестве ключей, поэтому нам необходимо реализовать методы equals() и hashCode(). Я не собираюсь писать для этого тесты, так как мы разрабатываем код в контексте рефакторинга. Дело в том, что от работоспособности этого кода жестко зависит срабатывание существующих тестов. Если код работает неправильно, существующие тесты не сработают. Однако в случае, если бы я программировал в паре с кем-то, кто плохо представлял бы себе направление дальнейшего движения, или в случае, если бы логика кода была бы несколько более сложной, я не
сомненно приступил бы к разработке специальных тестов.
Pair
public boolean equals(Object object) {
Pair pair= (Pair) object;
return from.equals(pair.from) && to.equals(pair.to);
}
public int hashCode() {
return 0;
}
0 - ужасное хэш-значение, однако такой метод хэширования легко реализовать, стало быть мы быстрее получим работающий код. Поиск валюты будет выполняться в соответствии с алгоритмом простого линейного поиска. Позже, когда у нас будет множество валют, мы сможем тщательнее проработать этот вопрос, используя реальные данные.
Теперь нам нужно место, в котором мы могли бы хранить значения обменных курсов:
Bank
private Hashtable rates= new Hashtable();
Нам также потребуется метод добавления нового курса обмена:
Bank
void addRate(String from, String to, int rate) {
rates.put(new Pair(from, to), new Integer(rate));
}
А также метод возврата значения обменного курса:
Bank
int rate(String from, String to) {
Integer rate= (Integer) rates.get(new Pair(from, to));
return rate.intValue();
}
Подождите-ка минутку! Перед нами красная полоса. Что случилось? Взглянув на код, мы обнаруживаем, что проблема в неправильном значении курса при обмене доллара на доллары. Мы ожидаем, что при обмене USD на USD курс обмена будет равен 1, однако на текущий момент это не так. Открытие было для нас сюрпризом, поэтому мы оформляем его в виде дополнительного теста:
public void testIdentityRate() {
assertEquals(1, new Bank().rate("USD", "USD"));
}
Теперь у нас три ошибки, однако все они могут быть исправлены при помощи одного небольшого изменения:
Bank
int rate(String from, String to) {
if (from.equals(to)) return 1;
Integer rate= (Integer) rates.get(new Pair(from, to));
return rate.intValue();
}
Зеленая полоска!
$5 + 10 CHF = $10, если курс обмена 2:1
$5 + $5 = $10
Операция $5 + $5 возвращает объект Money
Bank.reduce(Money)
Приведение объекта Money с одновременной конверсией валют
Reduce(Bank,String)
Далее мы переходим к нашему последнему, самому большому тесту, $5 + 10 CHF. В данной главе мы применили несколько важных технологий:
-добавили параметр, который, как мы ожидаем, нам понадобится;
-удалили дублирование между кодом и тестами;
-написали тест (testArrayEquals), чтобы проверить порядок функционирования встроенной операции Java;
-создали вспомогательный закрытый (private) класс, не обладающий собственными тестами;
-допустили ошибку при рефакторинге, написали еще один тест для того, чтобы изолировать проблему.
Экстремальное программирование: разработка через тестирование. Библиотека программиста. / К. Бек - СПб: Питер, 2003. - 224 с.
|