이 글은 '테스트 주도 개발 : By Example' 책 내용을 정리한 것입니다.
https://product.kyobobook.co.kr/detail/S000001032985
테스트 주도 개발은 자동화된 테스트로 개발을 이끌어가는 방식이다. 테스트 주도 개발에서는 다음의 두 가지 단순한 규칙만을 따른다.
- 어떤 코드건 작성하기 전에 실패하는 자동화된 테스를 작성하고, 오직 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성한다.
- 중복을 제거한다.
또한 위의 두 가지 규칙에 의해 프로그래밍 순서가 다음과 같이 결정된다.
- 빨강: 실패하는 작은 테스트를 작성한다. 처음에는 컴파일조차 되지 않을 수 있다.
- 초록: 빨리 테스트가 통과하게끔 만든다. 이를 위해 어떠한 죄악(함수가 무조건 특정 상수만을 반환하는 등)을 저질러도 좋다.
- 리팩토링: 일단 테스트를 통과하게만 하는 와중에 생겨난 모든 중복을 제거한다.
1부 화폐 예제
1장. 다중 통화를 지원하는 Money 객체
다중 통화를 지원하는 Money 객체부터 시작해보자. 다음과 같은 보고서가 있다고 가정하자.
다중 통화를 지원하는 보고서를 만들려면 통화 단위를 추가해야 한다.
또한 환율도 명시해야 한다.
새로운 보고서를 생성하려면 어떤 기능들이 있어야 할까? 즉, 어떤 테스트들이 있어야(이 테스트들이 모두 통과할 경우) 보고서에 제대로 계산되도록 하는 코드가 완성됐다는 것을 확신할 수 있을까?
- 통화가 다른 두 금액을 더해서 주어진 환율에 맞게 변한 금액을 결과로 얻을 수 있어야 한다.
- 어떤 금액을 어떤 수에 곱한 금액을 결과로 얻을 수 있어야 한다.
이런 식으로 앞으로 어떤 일을 해야 하는지 알려주고, 지금 하는 일에 집중할 수 있도록 도와주며, 언제 일이 다 끝나는지 알려줄 수 있게끔 할일 목록을 작성해보자.
객체를 만들면서 시작하는 것이 아닌 테스트를 먼저 만들어야 한다. 어떤 테스트가 필요할까? 할일 목록을 보니, 첫번째 테스트는 좀 복잡해보인다. 이럴때는 작은 것부터 시작하던지, 아니면 아예 손을 대지 않는게 좋다.
다음 항목인 곱하기를 보자. 대단히 어려워 보이지 않으니, 이걸 먼저 해보자.
테스트를 작성할 때는 오퍼레이션(객체가 수행할 수 있는 연산)의 완벽한 인터페이스에 대해 상상해보는 것이 좋다. 우리는 지금 오퍼레이션이 외부에서 어떤 식으로 보일지에 대한 이야기를 테스트 코드에 적고 있는 것이다. 우리 이야기가 언제나 현실이지는 않겠지만, 가능한 최선의 API에서 시작해서 거꾸로 작업하는 것이 현실적이게 하는 것 보다 낫다.
가장 빨리 초록색 막대를 보기 위해 테스트 코드를 작성하면 다음과 같다. (물론 위의 코드는 public 변수와, int형을 사용하는 등의 문제가 있다. 하지만 TDD는 우선 작은 단계부터 시작할 뿐이고, 이런 문제들은 추후에 수정될 것이다.)
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
위의 코드들은 다음과 같은 이유들로 컴파일 에러가 발생한다.
- Dollar 클래스가 없음
- 생성자가 없음
- times(int) 메소드가 없음
- amount 필드가 없음
우리는 위와 같은 컴파일 에러들을 순차적으로 해결하기 위해 코드를 작성하다 보면, 다음과 같은 Dollar 클래스를 얻게 된다.
public class Dollar {
int amount;
Dollar(int amount) {
}
void times(int multiplier) {
}
}
이제 컴파일 에러를 해결하고 테스트를 실행하면 빨간 막대를 보게 된다. 로그를 보면 10이 나와야하는데 0이 나왔다고 알려준다. 그리고 이제 우리의 목표는 '다중 통화 구현'이 아닌 '나머지 테스트 통과시키기' 이다.
지금 당장의 목표는 완벽한 해법을 구하는 것이 아니라, 테스트를 통과하는 것 뿐임을 잊지말자.
가장 단순히 테스트를 통과시키는 방법은 amount를 10으로 설정해주는 것이다.
int amount = 10;
이렇게 해주면 이제 우리는 TDD의 4번 과정까지 진행이 된 것이다.
- 재빨리 테스트를 하나 추가한다.
- 모든 테스트를 실행하고 새로 추가한 것이 실패하는지 확인한다.
- 코드를 조금 바꾼다.
- 모든 테스트를 실행하고, 전부 성공하는지 확인한다.
- 리팩토링을 통해 중복을 제거한다.
이제, 중복을 제거할 차례이다. 보통 중복을 찾기 위해 코드를 비교할 것이다. 하지만, 이번 경우에는 중복이 테스트에 있는 데이터와 코드에 있는 데이터 사이에 존재한다. 만약 코드를 다음과 같이 썼다면 어떨까?
int amount = 5 * 2
10을 5 * 2로 바꾸면 중복이 보인다. 5와 2라는 데이터가 테스트 코드와 클래스에 중복이 되는 것이다. 이를 제거하기 위해서는 amount와 multiplier를 정해줘야 하는데, 생성자에서는 amount를 times에서는 multiplier를 파라미터로 받도록 하자. 중복제거를 통해 탄생하게 된 Dollar 클래스는 다음과 같다.
public class Dollar {
int amount;
Dollar(int amount) {
this.amount = amount;
}
void times(int multiplier) {
amount *= multiplier;
}
}
이제 첫 번째 테스트에 완료 표시를 할 수 있게 됐다. 하지만 아직 할일이 끝난 것은 아니다. amount를 private로 만들고, 돈의 계산을 int로 하는 등의 문제는 여전히 남아 있다.
누군가는 위의 단계가 너무 작다고 느낄 수 있다. 하지만 TDD의 핵심은 이런 작은 단계를 밟는 것이 아니라, 이런 작은 단계를 밟는 능력을 갖추어야 한다는 것이다. 물론 항상 이런식으로 작업을 할 필요는 없겠지만, 일이 꼬이기 시작한다면 이런 능력이 필요하게 될 것이다.
다음 장에서는 Dollar 부작용에 대한 작업을 하게 될 것이다. 그 전에 지금까지 한 작업을 검토해보자.
- 우리가 알고 있는 작업해야 할 테스트 목록을 만들었다.
- 오퍼레이션이 외부에서 어떻게 보이길 원하는지 말해주는 이야기를 코드로 표현했다.
- JUnit에 대한 상세한 사항들은 잠시 무시하기로 했다.
- 스텁 구현을 통해 테스트를 컴파일 했다.
- 끔찍한 죄악을 범하여 테스트를 통과시켰다.
- 돌아가는 코드에서 상수를 변수로 변경하여 점진적으로 일반화 했다.
- 새로운 할일 등을 한번에 처리하는 대신, 할일 목록에 추가하고 넘어갔다.