[테스트 주도 개발] 10장_흥미로운 시간

두 times() 구현이 거의 비슷하다. 하지만 완전히 동일하지 않다.

1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return Money.franc(amount * multiplier);
}
}
1
2
3
4
5
6
7
8
9
class Dollar extends Money {
Dollar(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return Money.dollar(amount * multiplier);
}
}

팩토리 메서드를 인라인 시키면 어떨까 ?

1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Franc(amount * multiplier, "CHF");
}
}
1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Dollar(amount * multiplier, "USD");
}
}

바로 전 장에서 팩토리 메서드를 호출하는 것으로 바꿨었는데.. 실망스러운 일이다.
Franc 에서는 인스턴스 변수 currency 가 항상 ‘CHF’ 이므로,

1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Franc(amount * multiplier, currency);
}
}
1
2
3
4
5
6
7
8
9
class Dollar extends Money {
Dollar(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Dollar(amount * multiplier, currency);
}
}

Franc 를 가질지, Money 를 가질지 정말 중요한 사실인가 ? 고민하는 대신 그냥 수정하고 테스트를 돌려보자.

1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Money(amount * multiplier, currency);
}
}

컴파일러가, Money 를 Concrete 클래스로 바꿔야한닫고 한다.

1
2
3
4
5
6
7
class Money {
...

Money times(int multiplier){
return null;
}
}

빨간 막대다.
Expected :me.jko.tddstudy.Franc@262b2c86
Actual :me.jko.tddstudy.Money@371a67ec
더 나은 메세지를 보기 위해 toString() 을 정의하자.

1
2
3
4
// Money
public String toString(){
return amount + " " + currency;
}

테스트도 없이 코드를 작성했네 ? 테스트를 먼저 작성하는게 맞다. 하지만.

  1. 화면에 나타나는 결과를 확인하려던 참이었다.
  2. toString 은 디버그 출력으로만 쓰이기 떄문에, 잘못 구현됨으로 인해 얻게 될 리스크가 적다.
  3. 이미 빨간 막대 상태에서는 새로운 테스트를 작성하지 않는게 좋다.

이제 에러 메세지에,
expected: me.jko.tddstudy.Franc@262b2c86<10 CHF> but was: me.jko.tddstudy.Money@371a67ec<10 CHF>
조금 나아졌지만, 혼란스럽다. 답은 맞았는데 클래스가 다르다. Franc 대신 Money 가 나왔다. 문제는 여기에 있다.

1
2
3
4
5
6
// Money
public boolean equals(Object object) {
Money money = (Money) object;

return amount == money.amount && getClass().equals(money.getClass());
}

정말로 검사해야할 것은 클래스가 같은지가 아니라, currency 가 같은지 여부다.
빨간 막대 상황에서는 테스트를 추가로 작성하고 싶지 않다. 하지만 지금은 실제 모델 코드를 수정하려고 하는중이고 테스트 없이는 모델 코드를 수정할 수 없다. 변경된 코드를 되돌려서 다시 초록 막대 상태로 돌아가야한다.

1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Franc(amount * multiplier, currency);
}
}

다시 초록 막대이다. 우리 상황은 Franc(10, ‘CHF’) 와 Money(10, ‘CHF’) 가 서로 같기를 바라지만, 사실 그렇지 않다고 보고된 것이다. 이걸 그대로 테스트 해보자.

1
2
3
4
@Test
public void testDifferentClassEquality() {
assertTrue(new Money(10, "CHF").equals(new Franc(10, "CHF")));
}

예상 대로 실패한다. equals() 는 클래스가 아니라, currency 를 비교해야한다.

1
2
3
4
5
6
7
// Money
public boolean equals(Object object) {
Money money = (Money) object;

return amount == money.amount && currency().equals(money.currency());
}

이제 Franc.times() 에서 Money 를 반환해도 테스트가 여전히 통과한다.

1
2
3
4
5
6
7
8
9
class Franc extends Money {
Franc(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Money(amount * multiplier, currency);
}
}

Dollar.times() 에도 적용하자.

1
2
3
4
5
6
7
8
9
class Dollar extends Money {
Dollar(int amount, String currency) {
super(amount, currency);
}

Money times(int multiplier) {
return new Money(amount * multiplier, currency);
}
}

잘 돈다. 이제 두 구현이 동일해졌다. 위로 올리자.

1
2
3
4
// Money
Money times(int multiplier) {
return new Money(amount * multiplier, currency);
}

이제 아무것도 안 하는 하위 클래스들을 제거할 수 있겠다.


테스트 주도 개발 <켄트 벡>

Comments