Известный слоган гласит: «чистый код, который работает». Если вы будете решать проблему «чистый код» одновременно с проблемой «который работает», для вас это может оказаться слишком много. Как только вы поймете это, вернитесь обратно к решению проблемы «который работает» и только после этого принимайтесь за решение проблемы «чистый код».
При использовании шаблона «Очевидная реализация» (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}));
}
29. Шаблоны xUnit
В этой главе рассматриваются шаблоны, предназначенные для использования при работе с xUnit.
Проверка утвержденийКак убедиться в том, что тест работает правильно? Напишите логическое выражение, которое автоматически подтвердит ваше мнение о том, что код работает.
Если мы хотим сделать тесты полностью автоматическими, значит, абсолютно все предположения о работе тестируемого кода необходимо превратить в тесты, при этом результат выполнения этих тестов должен говорить нам, работает код корректно или нет. Проще говоря, мы должны обладать возможностью щелкнуть на кнопке и через короткое время узнать, работает код корректно или нет. Отсюда следует, что
• в результате выполнения теста должно получиться логическое значение: «истина» (True) указывает, что все в порядке, а «ложь» – что произошло нечто непредвиденное;
• проверка результата каждого теста выполняется компьютером автоматически при помощи какой-либо разновидности оператора assert().
Мне приходилось видеть выражения наподобие assertTrue(rectangle.area()!= 0). Чтобы тест выполнился успешно, метод area() должен вернуть любое ненулевое значение – это не очень полезный тест. Делайте тесты более конкретными. Если площадь прямоугольника должна быть равна 50, так и пишите: assertTrue(rectangle.area() == 50). Во многих реализациях xUnit присутствует специальное выражение assert() для тестирования равенства (эквивалентности). Отличительная его черта состоит в том, что вместо одного логического параметра выражение assertEquals() принимает два произвольных объекта и пытается определить, являются ли они эквивалентными. Преимущество состоит в том, что в случае неудачи выражение assertEquals() сгенерирует более информативное сообщение с указанием двух несовпадающих значений. Ожидаемое значение, как правило, указывается первым. Например, предыдущее выражение в среде JUnit можно переписать следующим образом: assertEquals(50, rectangle.area()).
Думать об объектах, как о черных ящиках, достаточно тяжело. Представим, что у нас есть объект Contract, состояние которого хранится в поле status и может быть экземпляром класса Offered или Running. В этом случае можно написать тест исходя из предполагаемой реализации:
Contract contract = new Contract(); // по умолчанию состояние Offered
contract.begin(); // состояние меняется на Running
assertEquals(Running.class, contract.status.class);
Этот тест слишком сильно зависит от текущей реализации объекта status. Однако тест должен завершаться успешно, даже если поле status станет логическим значением. Может быть, когда status меняется на Running, можно узнать дату начала работы над контрактом:
assertEquals(…, contract.startDate()); // генерирует исключение, если
// status является экземпляром Offered
Я признаю, что пытаюсь плыть против течения, когда настаиваю на том, что все тесты должны быть написаны только с использованием общедоступного (public) протокола. Существует специальный пакет JXUnit, который является расширением JUnit и позволяет тестировать значения переменных, даже тех, которые объявлены как закрытые.
Желание протестировать объект в рамках концепции белого ящика – это не проблема тестирования, это проблема проектирования. Каждый раз, когда у меня возникает желание протестировать значение переменной-члена, чтобы убедиться в работоспособности кода, я получаю возможность улучшить