빨강/초록/리팩토링
- 빨강: 실패하는 작은 테스트를 작성한다.
- 초록: 빨리 테스트가 통과하게만든다.
- 리팩토링: 테스트를 통과시키기 위해 생긴 중복을 제거한다.
들어가는 글
요구사항
- 달러로 명명된 채권을 다른 화폐의 채권을 다루도록 요구
상황
- 기존의 미국 달러로만 되어있는 로직을 다른 화폐에서도 적용하려고 하니 쉽지 않은 상황
해결 방법
- 가중평균 로직이 다른 통화에서도 적용되게 바꾸면 나머지 부분도 다중으로 바꿀 수 있을 것 같음
TDD를 위한 두 가지 단순한 법칙
- 어떤 코드건 작성하기 전에 실패하는 자동화된 테스트를 작성해라
- 중복을 제거해라
1. 다중 통화를 지원하는 Money 객체
보고서
종목 | 주 | 가격 | 합계 |
---|
IBM | 1000 | 25 | 25000USD |
GE | 400 | 100 | 40000CHF |
| | 합계 | 65000USD |
- 다중 통화를 지원하는 보고서를 위와 같이 정의하자
- 위의 환율을 통해 5USD == 10CHF 라는 것을 알 수 있다.
테스트 초안
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class MultipleCurrenciesTest {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
}
- 보고서를 토대로 위와 같이 테스트 코드를 작성할 수 있다. 하지만 컴파일 자체가 되지 않는 빨강 상태이다. (Dollar 객체 없음, 생성자 없음, times 메서드 없음, amount 필드 없음)
public class Dollar {
int amount = 5*2;
Dollar(int amount){
}
void times(int multiplier){
}
}
- 컴파일을 성공시키기 위해 달러 객체를 추가하고 amount에 10으로 초기화를 했다.
- 테스트는 빨강에서 초록 상태로 변경
중복 제거
- Dollar 객체의 amount 필드가가 10으로 초기화 되어있고, 테스트에서도 5와 2 인자로 받고 있어 중복이 있는 상태다.
- times 안에 5 * 2를 넣어 중복을 제거해보자
int amount;
void times(int multiplier){
amount = 5*2;
}
생성자와 times 인자로 중복 제거
public class Dollar {
int amount;
Dollar(int amount){
this.amount = amount;
}
void times(int multiplier){
amount *= multiplier;
}
}
- 여전히 amount는 10으로 하드코딩이 되어 있는 상태
- times와 생성자로 넘어온 인자로 amount의 값을 정하게 수정했다.
2. 타락한 객체
일반적인 TDD 주기
- 테스트를 작성한다.
- 실행 가능하게 만든다. (빨리 초록 막대를 보는 것이 중요)
- 올바르게 만든다. (중복 제거 등)
Dollar 객체 부작용
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class MultipleCurrenciesTest {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
five.times(3);
assertEquals(15, five.amount);
}
}
- 위와 같은 테스트를 통과할 수가 없다. five.amount는 15가 아니라 30이 나오므로 테스트 실패
- 위와 같은 상황을 별칭 문제라고 한다. (3장에서 자세히 나온다.) 이를 해결하기 위해 times의 리턴 값을 새로운 Dollar 객체를 만들게 수정한다.
- 그 전에 아래와 같이 테스트 코드를 다시 작성한다. 현재는 컴파일조차 되지 않는다.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class MultipleCurrenciesTest {
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(10, product.amount);
product = five.times(3);
assertEquals(15,product.amount);
}
}
- 그 다음, 컴파일을 하기 위해서 times의 리턴 값에 null을 반환하게 한다.
public class Dollar {
int amount;
Dollar(int amount){
this.amount = amount;
}
Dollar times(int multiplier){
amount *= multiplier;
return null;
}
}
- 지금 상황에서 테스트를 돌리면 컴파일은 되지만 실행되진 않는다. 테스트를 통과하기 위해 Dollar 객체를 리턴하도록 수정했다.
public class Dollar {
int amount;
Dollar(int amount){
this.amount = amount;
}
Dollar times(int multiplier){
return new Dollar(amount*multiplier);
}
}
초록색을 보기 위한 두 가지 전략
- 가짜로 구현하기 : 상수로 반환하게 만들고 진짜 코드를 얻을 때까지 단계적으로 상수를 변수로 바꿈
- 명백한 구현 사용하기: 살제 구현을 입력한다.
3. 모두를 위한 평균
- 위의 Dollar 객체 같이 객체를 값처럼 쓸 수 있는데, 이를 값 객체 패턴 (value object pattern) 이라고 한다.
- 이를 사용하면 별칭 문제에 대해 걱정할 필요가 없다는 장점이 있다. 예를 들면 수표가 하나 있는데 여기에 $5를 설정하고 또 다른 수표에도 아까 설정했던 $5를 설정했다고 치자. 두 번째 수표의 값을 설정함으로써 첫 번째 수표의 값까지 변하게 되는 문제가 생길 수 있다. 이를 별칭 문제라고 한다.
- 즉, 값 객체 패턴을 통해 새로운 객체 값을 만들어 반환하면 별칭 문제를 해결할 수 있다.
삼각측량 전략
- 만약 라디오 신호를 두 수신국이 감지하고 있을 때, 수신국 사이의 거리가 알려져 있고 각 수신국이 신호의 방향을 알고 있다면, 이 정보들만으로 충분히 신호의 거리와 방위를 알 수 있다. 이 계산법을 삼각측량이라고 한다.
- 즉 삼각 측량을 하기 위해서는 예제 코드가 최소 2개 이상은 있어야만 코드를 일반화 할 수 있다.
@Test
public void testEquality(){
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
}
public boolean equals(Object object){
Dollar dollar = (Dollar) object;
return amount == dollar.amount;
}
- 저자는 코드를 어떻게 리팩토링을 해야 하는지 전혀 감이 안 올 때만 삼각측량을 사용한다.
- 삼각측량은 코드와 테스트 사이에서 중복되는 테스트를 사용하기 때문에 비효율적
4. 프라이버시
- Dollar.times()는 호출 받은 객체의 값에 인자로 받은 곱수만큼 곱한 값을 갖는 Dollar를 반환하고 있다. 하지만 테스트는 Dollar를 비교하지 않고 Dollar의 field 값을 비교하고 있다.
Dollar 객체를 비교하게 테스트 변경
@Test
public void testMultiplication(){
Dollar five = new Dollar(5);
assertEquals(new Dollar(10), five.times(2));
assertEquals(new Dollar(15), five.times(3));
}
- 외부에서 amount 필드를 사용하지 않으니 접근 제어자를 private로 바꿀 수 있다.
- 테스트 내에서 직접 amount 필드에 접근하지 않으므로 테스트와 코드 사이의 결합도를 낮출 수있다.
private int amount;