[테스트 주도 개발] 7장_사과와 오렌지

제목 의미 : 서로 다른걸 비교할 수 없다.

Franc 와 Dollar 를 비교하면 어떻게 될까 ?

1
2
3
4
5
6
7
8
@Test
void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
assertTrue(new Franc(5).equals(new Franc(5)));
assertFalse(new Franc(5).equals(new Franc(6)));
assertFalse(new Franc(5).equals(new Dollar(5)));
}

실패한다. 오직 금액과 클래스가 동일할 때만 두 Money 가 서로 같은 것이다.

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

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

자바 객체의 용어를 사용하는 것보다 재정 분야에 맞는 용어를 사용하고 싶다. currency 통화 개념 같은 게 없고, 통화 개념을 도입할 충분한 이유가 없으므로 잠시 이렇게 두자.


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

Read more

[테스트 주도 개발] 8장_객체 만들기

times() 의 구현 코드가 거의 같다.

1
2
3
4
5
6
7
8
9
// Franc Class
Franc times(int multiplier) {
return new Franc(amount * multiplier);
}

// Dollar Class
Dollar times(int multiplier) {
return new Dollar(amount * multiplier);
}

양쪽 모두 Money 를 반환하게 만들면 더 비슷해진다.

1
2
3
4
5
6
7
8
9
// Franc Class
Money times(int multiplier) {
return new Franc(amount * multiplier);
}

// Dollar Class
Money times(int multiplier) {
return new Dollar(amount * multiplier);
}

이제, Money 의 두 하위 클래스는 많은 일을 하지 않으므로 제거하고 싶다. 한번에 이렇게 큰 단계를 밟는 것은 TDD 를 효과적으로 보여주기 적절치 않을 것 같다.
하위 클래스에 대한 직접적인 참조가 적어지면 하위 클래스를 제거하기 쉬울 것 같다.

1
2
3
4
5
6
7
@Test
void testMultiplication() {
Dollar five = Money.dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}

구현 코드는 Dollar 를 생성해 반환한다.

1
2
3
4
// Money Class
static Dollar dollar(int amount) {
return new Dollar(amount);
}

Test 선언부도 다음과 같이 변경한다.

1
2
3
4
5
6
7
@Test
void testMultiplication() {
Money five = Money.dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}

Read more

[테스트 주도 개발] 9장_우리가 사는 시간

제목 : 영어에서 시간과 곱하기가 모드 ‘times’ 라는 점에서 착안한 말장난이다.
통화 개념을 어떻게 테스트하길 원하는가 ? 통화를 표현하기 위한 복잡한 객체를 만들 수도 있다. 그리고, 그 객체들이 필요한 만큼만 만들어지도록 하기 위해 flyweight factories 경량 팩토리를 사용할 수 있을 것이다. 하지만, 당분간 대신 문자열을 쓰자.

1
2
3
4
5
6
@Test
void testCurrency(){
assertEquals("USD", Money.dollar(1).currency);
assertEquals("CHF", Money.franc(1).currency);
}

우선, Money 에 currency() 메서드를 선언하자.

1
abstract String currency();

그리고 이를, 두 하위 클래스에서 구현하자.

1
2
3
4
5
6
7
8
9
10
11
// Dollar Class
@Override
String currency() {
return "USD";
}

// Franc Class
@Override
String currency() {
return "CHF";
}

우린 두 클래스를 모두. 포함할 수 있는 동일한 구현을 원한다. 통화를 인스턴스 변수에 저장하고, 메서드에서는 그냥 그걸 반환하게 할 수 있을 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Franc extends Money {
private String currency;

Franc(int amount) {
this.amount = amount;
currency = "CHF";
}

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

@Override
String currency() {
return currency;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dollar extends Money {
private String currency;

Dollar(int amount) {
this.amount = amount;
currency = "USD";
}

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

@Override
String currency() {
return currency;
}
}

이제 두 currency() 가 동일하므로 변수 선언과 currency() 구현을 모두 위로 올릴 수 있다.

Read more

[테스트 주도 개발] 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 를 가질지 정말 중요한 사실인가 ? 고민하는 대신 그냥 수정하고 테스트를 돌려보자.

Read more

[테스트 주도 개발] 11장_모든 악의 근원

두 하위 클래스에는 이제 생성자밖에 없다. 제거하자.

코드의 의미를 변경하지 않으면서 하위 클래스에 대한 참조를 상위 클래스에 대한 참조로 변경할 수 있다.

1
2
3
4
5
6
7
8
//Money
static Money dollar(int amount) {
return new Money(amount, "USD");
}

static Money franc(int amount) {
return new Money(amount, "CHF");
}

이제 Dollar 에 대한 참조가 하나도 없으므로 지울 수 있다. 하지만, Franc 는 테스트 코드에서 아직 참조한다.

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

이 테스트를 지워도 될 정도로 다른 곳에서 이미 동치성 테스트를 충분히 하고 있는가 ?

1
2
3
4
5
6
7
8
@Test
void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5))); // 1
assertFalse(Money.dollar(5).equals(Money.dollar(6))); // 2
assertTrue(Money.franc(5).equals(Money.franc(5))); // 3
assertFalse(Money.franc(5).equals(Money.franc(6))); // 4
assertFalse(Money.franc(5).equals(Money.dollar(5)));
}

3, 4 단언은 1, 2 단언과 중복이다. 지우자.

1
2
3
4
5
6
@Test
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)));
}

Read more

[테스트 주도 개발] 12장_드디어 더하기

전체 더하기 기능에 대한 스토리를 어떻게 적어야할지 모르겠다. 간단한 예, “5 달러 + 5달러 = 10 달러” 에서 시작하자.

1
2
3
4
5
@Test
void testSimpleAddition() {
Money sum = Money.dollar(5).plus(Money.dollar(5));
assertEquals(Money.dollar(10), sum);
}

그냥 Money.dollar(10) 을 반환하는 식으로 가짜 구현을 할 수 있지만, 어떻게 구현할지 명확하므로 다음과 같이 하겠다.

1
2
3
4
// Money
Money plus(Money addend){
return new Money(amount + addend.amount, currency);
}

다중 통화 사용에 대한 내용을 시스템의 나머지 코드에 숨기고 싶다. ( 2달러 + 3CHF ) * 5 를 보자. Money 가 수식의 가장 작은 단위가 된다. 연산의 결과로 Expression 들이 생기는데, 그 중 하나가 sum 이다. 연산이 완료되면 환율을 이용해서 결과 expression 을 단일 통화로 축약할 수 있다. 이런식으로 테스트할 수 있다.

1
2
3
4
5
@Test
void testSimpleAddition() {
....
assertEquals(Money.dollar(10), reduced);
}

reduced 란 이름의 expression 은 expression 에 환율을 적용해서 얻어진다. 실세계에서 환율이 적용되는 곳은 어디인가 ? 은행이다.

1
2
3
4
5
6
@Test
void testSimpleAddition() {
...
Money reduced = back.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}

여기서, 단순히 “…reduce = sum.reduce(“USD”, back)” 라고 할수 있었다. 왜 Back 가 reduce() 를 수행할 책임을 맡았을 까 ?

  1. Expression 은 하려고 하는 일의 핵심이다. 핵심이 되는 객체가 다른 부분에 대해서 될 수 있는 한 모르도록 하자. 그러면, 핵심 객체가 가능한 오랫 동안 유연할 수 있다. 그리고, 테스트하기도 쉽고 재활용하거나 이해하기에 쉽다.
  2. Expression 과 관련이 있는 오퍼레이션이 많은 수 있다. 모든 오펴레이션을 Expression 에만 추가하면 Expression 는 무한히 커진다.
Read more

[테스트 주도 개발] 4장_프라이버시

1
2
3
4
5
6
7
8
@Test
void testMultiplication() {
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(10, product.amount);
product = five.times(3);
assertEquals(15, product.amount);
}

첫 번째 assertion 을 Dollar 와 Dollar 를 비교하는 것으로 재작성할 수 있다.

1
2
3
4
5
6
7
8
@Test
void testMultiplication() {
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(new Dollar(10), product);
product = five.times(3);
assertEquals(15, product.amount);
}

두 번째 assertion 도 마찬가지다.

1
2
3
4
5
6
7
8
@Test
void testMultiplication() {
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(new Dollar(10), product);
product = five.times(3);
assertEquals(new Dollar(15), product);
}

이제 임시 변수인 product는 필요없다.

1
2
3
4
5
6
@Test
void testMultiplication() {
Dollar five = new Dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}

테스트를 고치고 나니, Dollar 의 amount 인스턴스 변수를 사용하는 코드는 Dollar 자신 밖에 없다. 따라서, 변수를 private 으로 변경 가능하다.

1
private int amount;

동치성 테스트가, 동치성에 대한 코드가 정확히 동작하는 것을 검증하는데 실패하면다면 곱하기 테스트 역시, 곱하기에 대한 코드가 정확히 동작한다는 것을 검증하는데 실파헤가 된다. 이것은 TDD 를 하면서 적극적으로 관리해야할 위험 요소이다.

Read more

[테스트 주도 개발] 5장_솔직히 말하자면

다음을 테스트해보자.

1
5 + 10CHF = $10 (환율이 2:1 일 경우)

우선 Dollar 객체와 비슷하지만 프랑을 표현할 수 있는 객체가 필요하다. Dollar 테스트를 복사한 후 수정해보자.

1
2
3
4
5
6
7
@Test
void testFrancMultiplication() {
Franc five = new Franc(5);
assertEquals(new Franc(10), five.times(2));
assertEquals(new Franc(15), five.times(3));
}

4장에서 테스트를 단순화해놓아서 지금하는 작업이 더 쉬워졌다. 테스트 주기에는 서로 다른 단계들이 있다는 것을 다시 정리하자.

  1. 테스트 작성
  2. 컴파일 되게하기
  3. 실패하는지 확인하기 위해 실행
  4. 실행하게 만듦
  5. 중복 제거

처음 네 단계는 빨리 진행해야한다. 또한, 다섯 번째 단계 없이는 앞의 네 단계도 제대로 되지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Franc {
private int amount;

Franc(int amount) {
this.amount = amount;
}

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

public boolean equals(Object object) {
Franc franc = (Franc) object;

return amount == franc.amount;
}
}

중복이 엄청나게 많기 때문에, 다음 테스트를 작성하기 전에 이것들을 제거해야한다. equals() 를 일반화하는 것부터 시작하자.


Read more

[테스트 주도 개발] 6장_돌아온 모두를 위한 평등

5장에서 테스트를 빨리 통과하기 위해 코드를 복사해서 붙이는 죄를 저질렀다. 이제 청소할 시간이다.
Money 클래스가 공통의 equals 코드를 갖게 하면 어떨까 ? 간단한 것부터 시작하자.

1
2
class Money {
}

테스트는 여전히 돌아간다. Dollar 가 Money 를 상속받아도 여전히 어떤 것도 깨지지 않는다.

1
2
3
public class Dollar extends Money {
...
}

이제 amount 인스턴스 변수를 Money 로 옮길 수 있다.

1
2
3
class Money {
protected int amount;
}

하위 클래스에서도 변수를 볼 수 있도록, 가시성을 private 에서 protected 로 변경했다.
이제, equals() 를 위로 올릴 수 있다. 우선, 임시 변수 선언하는 부분을 변경하자.

1
2
3
4
5
public boolean equals(Object object) {
Money dollar = (Dollar) object;

return amount == dollar.amount;
}

모든 테스트가 잘 돈다. 이제 cast 부부을 변경하자.

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

return amount == dollar.amount;
}
Read more

[테스트 주도 개발] 3장_모두를 위한 평등

Dollar 객체같이 객체를 값처럼 쓸 수 있는 것을 객체 Value Object Pattern 이라고 한다. 값 객체에 대한 제약사항 중 하나는 객체의 인스턴스 변수가 생성자를 통해서 일단 설정된 후에는 변하지 않는다는 것이다.
값 객체가 암시하는 것은,

  1. 모든 연산은 새 객체를 반환해야한다.
  2. 값 객체는 equals() 를 구현해야한다.
1
2
3
void testEquality(){
assertTrue(new Dollar(5).equals(new Dollar(5)));
}

빨간 막대다. 가짜로 구현하는 방법은 단순히 true 를 반환하도록 하는 것이다.

1
2
3
public boolean equals(Object object){
return true;
}

(삼각측량) 예제가 두 개 이상 있으면 코드를 일반화 할 수 있다. 다음과 같이 5!=6 을 해보자.

1
2
3
4
void testEquality(){
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
}

이제 equlity 를 일반화하자.

1
2
3
4
public boolean equals(Object object){
Dollar dollar = (Dollar) object;
return amount == dollar.amount;
}

이렇게 어떻게 리팩토링해야하는지 감이 안오면 삼각측량을 사용하자. 코드와 테스트 사이의 중복을 제거하고 일반적인 해법을 구할 방법이 보이면 그냥 그 방법대로 구현하면 된다.
자, 동일성 문제는 일시적으로 해결했다. 널 값이나 다른 객체들과 비교하는 상황은 일반적이진 않지만 당장은 필요하지 않다. 할일 목록에 적어 두기만 하자.

Read more