• 어떤 코드건 작성하기 전에 실패하는 자동화된 테스트를 작성하라
• 오직 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성하라
• 중복을 제거하라
📌 TDD 리듬
- 재빨리 테스트를 하나 추가한다.
- 모든 테스트를 실행하고 새로 추가한 것이 실패하는지 확인한다.
- 코드를 조금 바꾼다.
- 모든 테스트를 실행하고 전부 성공하는지 확인한다.
- 리팩토링을 통해 중복을 제거한다.
✒️ 프로그래밍 순서
1. 빨강: 실패하는 작은 테스트를 작성하라. 처음에는 컴파일조차 되지 않을 수 있다.
2. 초록: 빨리 테스트가 통과하게끔 한다. 이를 위해 어떠한 죄악을 저질러도 좋다.
3. 리팩토링: 일단 테스트를 통과하게만 하는 와중에 생긴 모든 중복을 제거하라.
📌 As-is
다음과 같은 보고서가 있다고 하자.
종목 | 주 | 가격 | 합계 |
IBM | 1,000 | 25 | 25,000 |
GE | 400 | 100 | 40,000 |
합계 | 65,000 |
다중 통화를 지원하는 보고서를 만들려면 통화 단위를 추가해야 한다.
종목 | 주 | 가격 | 합계 |
IBM | 1,000 | 25USD | 25,000USD |
Novartis | 400 | 100CHF | 40,000CHF |
합계 | 65,000USD |
환율도 명시해야 한다.
기준 | 변환 | 환율 |
CHF | USD | 1.5 |
예를 들어, $5 + 10CF = $10(환율이 2:1인 경우)라는 결과가 나와야 한다.
📌 필요한 테스트 코드
💡 객체를 만들면서 시작하는 게 아니라 테스트를 먼저 만들어야 한다.
- 통화가 다른 두 금액을 더해서 주어진 환율에 맞게 변한 금액 결과를 얻을 수 있어야 한다.
- 어떤 금액(주가)을 어떤 수(주식의 수)에 곱한 금액을 결과로 얻을 수 있어야 한다.
첫 번째는 좀 복잡해보이니, 두 번째 항목부터 다뤄보자.
📌 테스트 작성
테스트를 작성할 때는 오퍼레이션(이에 대한 특정한 하나의 구현이 메서드)의 완벽한 인터페이스를 상상해보라.
가능한 최손의 API에서 시작해서 거꾸로 작업하는 것이 좋다.
class DollarTest {
@Test
void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
}
위 코드들은 다음과 같은 컴파일 에러 원인이 있다.
- Doller 클래스 부재
- 생성자 없음
- times(int) 메서드 없음
- amount 필드 없음
한 번에 하나씩 정복해보면 아레와 같은 코드를 손 쉽게 얻을 수 있다.
public class Dollar {
int amount;
Dollar(int amount) {}
void times(int multiplier) {}
}
컴파일 에러를 해결하고 테스트를 실행하면 (공포의) 빨간 막대를 보게 된다.
기댓값이 10이지만 실제로는 0이 출력된 것이다.
이제 목표는 '다중 통화 구현'이 아닌 '나머지 테스트 통과시키기'로 바뀌었다.
📌 초록색 막대를 위한 최소 작업
int amount = 10;
이 해법이 마음에 들진 않을 수는 있으나, 당장 중요한 것은 테스트를 통과하는 것이다.
amount에 10을 설정해주면 테스트가 통과되고, 우리는 TDD의 4번 과정까지 진행이 된 것이다.
- 재빨리 테스트를 하나 추가한다.
- 모든 테스트를 실행하고 새로 추가한 것이 실패하는지 확인한다.
- 코드를 조금 바꾼다.
- 모든 테스트를 실행하고 전부 성공하는지 확인한다.
- 리팩토링을 통해 중복을 제거한다.
📌 의존성과 중복
- 스티브 프리만은 테스트와 코드 간의 문제는 중복이 아니라, 사이에 존재하는 의존성이라고 지적했다.
- 코드를 바꾸지 않으면서도 의미 있는 테스트를 하나 더 작성하는 것은 현재의 구현으로 불가능하다.
- 의존성이 문제 그 자체라면, 중복은 문제의 징후다. (특히 로직의 중복이 가장 흔하다.)
- 프로그램에서는 중복만 제거해주면 의존성도 제거된다.
- 즉, 다음 테스트로 진행하기 전에 중복을 제거함으로써, 오직 한 가지(one and only once)의 코드 수정을 통해 다음 테스트도 통과되게 만들 가능성을 최대화하는 것이다.
✒️ One and only once : 필요한 것을 하되(once) 단 한 번만(only once) 하라
📌 중복 제거
int amount = 5 * 2;
데이터와 코드 사이에 존재하는 중복을 확인할 수 있다. (실제로도 풀어서 쓰는 것이 맞다.)
이를 제거하기 위해 amount와 multiplier를 정해주어야 한다.
amount는 생성자에서 받으면 될 것이고, multiplier는 times 메서드의 인자로 대체할 수 있다.
public class Dollar {
int amount;
public Dollar(int amount) {
this.amount = amount;
}
public void times(int multiplier) {
amount *= multiplier;
}
}
📌 남은 일 확인
$5 + 10CHF = $10(환율이 2:1일 경우)$5 * $2 = $10
amount를 private로 만들기
Dollar 부작용?
Money 반올림?
- 우리가 알고 있는 작업해야 할 테스트 목록을 만들었다.
- 오퍼레이션이 외부에서 어떻게 보이길 원하는지 말해주는 이야기를 코드로 표현했다.
- JUnit에 대한 상세한 사항들은 잠시 무시하기로 했다.
- 스텁 구현을 통해 테스트를 컴파일했다.
- 끔찍한 죄악을 범하여 테스트를 통과시켰다.
- 돌아가는 코드에서 상수를 변수로 변경하여 점진적으로 일반화했다.
- 새로운 할 일들을 한번에 처리하는 대신 할일 목록에 추가하고 넘어갔다.