мы предпочитаем не писать новых тестов, пока не избавимся от красной полосы.

Обстоятельства приняты к сведению.

Теперь сообщение об ошибке изменилось: "expected:<1 °CHF> but was:<1 °CHF>". Выглядит осмысленней, однако сбивает с толку. В двух объектах хранятся одни и те же данные, однако при этом объекты не считаются равными. Проблема кроется в реализации метода equals():

Money

public boolean equals(Object object) {

Money money = (Money) object;

return amount == money.amount

&& getClass(). equals(money.getClass());

}

В данном случае происходит сравнение имен классов, в то время как логичнее сравнивать идентификаторы валют.

Лучше не писать никаких новых тестов, если перед вами красная полоса. Однако нам нужно внести изменения в разрабатываемый код, и мы не можем изменить код, не обладая соответствующим тестом. Консервативный подход заключается в том, чтобы отменить изменение, которое привело к появлению красной полосы. В этом случае мы вновь получим зеленую полосу. После этого мы сможем модифицировать тест для метода equals(), исправить его реализацию и вновь применить изначальное изменение.

В данном случае мы будем действовать консервативно. (Иногда я плюю на все и пишу тест, не обращая внимания на красную полосу, однако я поступаю так, только когда дети уже спят.)

Franc

Money times(int multiplier) {

return new Franc (amount * multiplier, currency);

}

Перед нами снова зеленая полоса. Мы попали в ситуацию, когда объект Franc(10,"CHF") не равен объекту Money(10,"CHF"), хотя нам хотелось бы, чтобы эти объекты были равны. Превращаем наше желание в тест:

public void testDifferentClassEquality() {

assertTrue(new Money(10, "CHF"). equals(new Franc(10, "CHF")));

}

Как и ожидалось, тест потерпел неудачу. Код метода equal() должен сравнивать идентификаторы валют, а не имена классов:

Money

public boolean equals(Object object) {

Money money = (Money) object;

return amount == money.amount

&& currency(). equals(money.currency());

}

Теперь метод Franc.times() может возвращать значение Money, и все тесты будут по-прежнему успешно выполняться:

Franc

Money times(int multiplier) {

return new Money(amount * multiplier, currency);

}

Сработает ли этот трюк для метода Dollar.times()?

Dollar

Money times(int multiplier) {

return new Money (amount * multiplier, currency);

}

Да! Теперь две реализации абсолютно идентичны, и мы можем переместить их в базовый класс.

Money

Money times(int multiplier) {

return new Money(amount * multiplier, currency);

}

$5 + 1 °CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 1 °CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)

Валюта?

Нужен ли тест testFrancMultiplication()?

Метод умножения там, где ему следует быть, теперь мы готовы удалить ненужные нам производные классы.

В данной главе мы

• сделали идентичными две реализации метода times(), для этого мы избавились от вызовов фабричных методов в них, и заменили константы переменными;

• добавили в класс отладочный метод toString() без теста;

• попробовали модифицировать код (заменили тип Franc возвращаемого значения на Money) и обратились к тестам, чтобы узнать, сработает ли это;

• отменили изменения и написали еще один тест, добились успешного выполнения теста и вновь применили изменения.

11. Корень всего зла

$5 + 1 °CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 1 °CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)

Валюта?

Нужен ли тест testFrancMultiplication()?

Два производных класса, Dollar и Franc, обладают только конструкторами, однако конструктор – это недостаточная причина для создания подкласса. Мы должны избавиться от бесполезных подклассов.

Ссылки на подклассы можно заменить ссылками на суперкласс, не изменив при этом смысл кода. Начнем с класса Franc:

Franc

static Money franc(int amount) {

return new Money (amount, «CHF»);

}

Затем перейдем к классу Dollar:

Dollar

static Money dollar(int amount) {

return new Money (amount, «USD»);

}

Ссылок на класс Dollar больше нет, поэтому мы можем удалить этот класс. Однако в только что написанном нами тесте есть одна ссылка на класс Franc:

public void testDifferentClassEquality() {

assertTrue(new Money(10, "CHF"). equals(new Franc(10, "CHF")));

}

Если равенство объектов достаточно хорошо протестировано другими тестами, значит, мы можем безбоязненно удалить этот тест. Давайте взглянем на другие тесты:

public void testEquality() {

assertTrue(Money.dollar(5). equals(Money.dollar(5)));

assertFalse(Money.dollar(5). equals(Money.dollar(6)));

assertTrue(Money.franc(5). equals(Money.franc(5)));

assertFalse(Money.franc(5). equals(Money.franc(6)));

assertFalse(Money.franc(5). equals(Money.dollar(5)));

}

Похоже, что все возможные случаи определения равенства достаточно полно охвачены другими тестами. Я даже сказал бы, что тестов слишком много. Мы можем удалить третье и четвертое выражение assert, так как они дублируют первое и второе:

public void testEquality() {

assertTrue(Money.dollar(5). equals(Money.dollar(5)));

assertFalse(Money.dollar(5). equals(Money.dollar(6)));

assertFalse(Money.franc(5). equals(Money.dollar(5)));

}

$5 + 1 °CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату