28. Шаблоны зеленой полосы
Когда у вас есть сломанный тест, вы должны заставить его работать. Если вы рассматриваете красную полосу как состояние, из которого следует выйти как можно быстрее, вы должны овладеть приемами быстрого получения зеленой полосы. Используйте следующие шаблоны, чтобы заставить ваш тест выполниться (даже если полученный в результате этого код не просуществует и часа).
Подделка (Fake It)
Если у вас есть тест, завершающийся неудачей, какой должна быть самая первая реализация? Сделайте так, чтобы тестируемый метод возвращал константу. После того как тест начал работать, постепенно трансформируйте константу в выражение с использованием переменных.
Пример использования этого подхода продемонстрирован в ходе разработки нашей реализации xUnit. Вначале мы использовали строковую константу:
return «1 run, 0 failed»
Затем эта строка была преобразована в выражение:
return «%d run, 0 failed» % self.runCount
Однако этим дело не кончилось. В конце мы получили выражение:
return «%d run, %d failed» % (self.runCount, self failureCount)
Шаблон «Подделка» (Fake It) напоминает страховочную веревку, которая соединяет вас с верхней точкой маршрута, когда вы карабкаетесь по скале. Пока что вы еще не забрались на самый верх (тест на месте и работает, но тестируемый код некорректен). Однако в любой точке маршрута вы держитесь за веревку и знаете, что когда достигнете самого верха, то будете в безопасности (тест работает в ходе рефакторинга, а также после получения окончательного кода).
Шаблон «Подделка» (Fake It) многим может показаться совершенно бесполезным. Зачем писать код, который абсолютно точно придется заменить другим? Дело в том, что иметь хоть какой-то работающий код – это лучше, чем вообще не иметь работающего кода, в особенности если у вас есть тесты, которые могут доказать работоспособность кода. Петер Хансен (Peter Hansen) рассказал мне следующую историю:
Буквально вчера два новичка в области TDD – мой партнер и я – решили в точности следовать букве закона. То есть мы написали тест, а затем написали самый простой, но совершенно бесполезный код, который обеспечивал срабатывание теста. Пока мы писали этот код, мы обнаружили, что тест написан неправильно.
Каким образом поддельная реализация подсказала им, что написанный ими тест некорректен? Я понятия не имею, однако я счастлив, что они вовремя обнаружили это. Быть может, если они не воспользовались бы поддельной реализацией, они пошли бы по ложному пути. Возможно, исправление связанных с этим ошибок обошлось бы им дороже.
При использовании шаблона «Подделка» (Fake It) возникает как минимум два положительных эффекта:
Психологический. Если перед вами зеленая полоса, вы чувствуете себя совершенно иначе, чем когда перед вами красная полоса. Когда полоса зеленая, вы знаете, на чем стоите. Вы можете смело и уверенно приступать к рефакторингу.
Контроль над объемом работы. Программисты привыкли пытаться предвидеть появление в будущем самых разнообразных проблем. Если вы начинаете с конкретного примера и затем осуществляете обобщение кода, это помогает вам избавиться от лишних опасений. Вы можете сконцентрироваться на решении конкретной проблемы и поэтому выполнить работу лучше. При переходе к следующему тесту вы опять же концентрируетесь на нем, так как знаете, что предыдущий тест гарантированно работает.
Нарушает ли шаблон «Подделка» (Fake It) правило о том, что не следует писать код, который вам не потребуется? Я так не думаю, ведь на этапе рефакторинга вы удаляете дублирование данных между тестом и тестируемым кодом. Допустим, я написал:
assertEquals(new MyDate(«28.2.02»), new MyDate(«1.3.02»). yesterday());
MyDate
public MyDate yesterday() {
return new MyDate("28.2.02");
}
Между тестом и кодом существует дублирование. Попробуем исправить ситуацию:
MyDate
public MyDate yesterday() {
return new MyDate(new MyDate("1.3.02"). days()-1);
}
Однако дублирование по-прежнему присутствует. Чтобы избавиться от него, заменяем MyDate(«1.3.02») на this (в моем тесте эти значения равны). Получается:
MyDate
public MyDate yesterday() {
return new MyDate(this.days()-1);
}
Однако увидеть возможность подобных подстановок с первого взгляда удается далеко не всегда и далеко не всем, поэтому для пущей ясности вы можете использовать триангуляцию, по крайней мере до тех пор, пока вам не надоест. Когда вам надоест, вы чаще будете пользоваться шаблоном «Подделка» (Fake It) или «Очевидная реализация» (Obvious Implementation).
Триангуляция (Triangulate)
Какой самый консервативный способ позволяет формировать абстракцию при помощи тестов? Делайте код абстрактным только в случае, если у вас есть два или более примера.
Рассмотрим пример. Предположим, мы хотим написать функцию, которая возвращает сумму двух целых чисел. Мы пишем:
public void testSum() {
assertEquals(4, plus(3, 1));
}
private int plus(int augend, int addend) {
return 4;
}
Чтобы получить представление о правильном дизайне, мы добавляем еще один пример:
public void testSum() {
assertEquals(4, plus(3, 1));
assertEquals(7, plus(3, 4));
}
Теперь, когда у нас есть еще один пример, мы можем сделать реализацию метода plus() абстрактной:
private int plus(int augend, int addend) {
return augend + addend;
}
Триангуляция выглядит привлекательно, так как правила ее выполнения вполне понятны. Правила для шаблона «Подделка» (Fake It) основаны на ощущении дублирования кода между тестом и поддельным кодом. Это ощущение может быть субъективным, поэтому правила выглядят несколько туманными. Несмотря на то, что они кажутся простыми, правила триангуляции создают замкнутый цикл. После того как мы написали два выражения assert и сформировали абстрактную корректную реализацию метода plus(), мы можем уничтожить одно из выражений assert, так как теперь оно является избыточным. А сделав это, мы сможем упростить реализацию plus(), чтобы этот метод возвращал константу. После этого нам надо будет снова добавить выражение assert.
Я использую триангуляцию только в случае, если я действительно не уверен, какая абстракция является корректной. В других ситуациях я предпочитаю использовать шаблон «Подделка» (Fake It) или «Очевидная реализация» (Obvious Implementation).
Очевидная реализация (Obvious Implementation)
Как реализоать простую операцию? Просто реализуйте ее.
Шаблоны «Подделка» (Fake It) и «Триангуляция» (Triangulate) позволяют вам двигаться маленькими шажками. Но иногда вы абсолютно уверены в том, как можно корректно реализовать операцию. Вперед! Пишите то, что вы думаете. Например, должен ли я использовать шаблон «Подделка» (Fake It) для реализации чего-либо столь же простого, как метод plus()? Как правило, нет. Обычно для таких простых методов я просто пишу очевидную реализацию. Если при этом передо мной неожиданно появляется красная полоса за красной полосой, я перехожу на более короткий шаг.
В шаблонах «Подделка» (Fake It) и «Триангуляция» (Triangulate) не существует никакой особенной добродетели. Если вы знаете, что писать, и если это получится достаточно быстро, то смело пишите готовый код. Однако помните, что, используя только очевидную реализацию, вы требуете от себя совершенства. С психологической точки зрения это может быть разрушительный ход. Что, если написанное вами на самом деле не является самым простым изменением, которое заставляет тест работать? Что, если ваш партнер покажет вам еще более простой вариант кода? Вы проиграли! Ваш мир рухнул! Вы в ступоре.
Известный слоган гласит: «чистый код, который работает». Если вы будете решать проблему «чистый код» одновременно с проблемой «который работает», для вас это может оказаться слишком много. Как только вы поймете это, вернитесь обратно к решению проблемы «который работает» и только после этого принимайтесь за решение проблемы «чистый код».
При использовании шаблона «Очевидная реализация» (Obvious Implementation) следите за тем, насколько часто вы сталкиваетесь с красной полосой. Часто приходится попадать в ловушку: я записываю очевидную реализацию, но она не работает. Но теперь я точно знаю, что именно я должен написать. Поэтому я вношу в код изменения. Однако тест по-прежнему не работает. Но теперь-то я уж точно знаю… Это часто случается при возникновении ошибок типа «индекс отличается на единицу» и «положительные/отрицательные числа».
Прежде всего вы должны следить за соблюдением ритма красный – зеленый – рефакторинг. Очевидная реализация – это вторая передача. Будьте готовы снизить скорость, если ваш мозг начинает выписывать чеки, которые не могут быть оплачены вашими пальцами.
От одного ко многим (One to Many)
Как реализовать операцию с коллекцией объектов? Сначала реализуйте эту операцию, манипулирующую единственным объектом, затем модернизируйте ее для работы с коллекцией таких объектов.
Например, предположим, что мы разрабатываем функцию, которая суммирует массив чисел. Мы можем начать с одного числа:
public void testSum() {
assertEquals(5, sum(5));
}
private int sum(int value) {
return value;
}
(Я добавил метод sum() в класс TestCase, чтобы не создавать новый класс ради одного метода.)
Теперь мы хотим протестировать sum(new int[] {5, 7}). Для начала добавим в метод sum() параметр, соответствующий массиву целых чисел:
public void testSum() {
assertEquals(5, sum(5, new int[] {5}));
}
private int sum(int value, int[] values) {
return value;
}
Этот этап можно рассматривать как пример применения шаблона «Изоляция изменения» (Isolate Change). После того как мы добавили параметр, мы можем менять реализацию, не затрагивая код теста.
Теперь мы можем использовать коллекцию вместо единственного значения:
private int sum(int value, int[] values) {
int sum = 0;
for (int i = 0; i < values.length; i++)
sum += values[i];
return sum;
}
Теперь можно удалить неиспользуемый параметр:
public void testSum() {
assertEquals(5, sum(new int[] {5}));
}
private int sum(int[] values) {
int sum = 0;
for (int i = 0; i < values.length; i++)
sum += values[i];
return sum;
}
Предыдущий шаг – это тоже демонстрация шаблона «Изоляция изменения» (Isolate Change). Мы изменили код и в результате можем менять тест, не затрагивая код. Теперь мы можем расширить тест, как планировали:
public void testSum() {
assertEquals(12, sum(new int[] {5, 7}));
}