무엇을 테스트 할 지 모르기 때문이다.
무엇을 테스트 할 지 모르기 때문에 TDD가 유행했다고 생각한다.
TDD는 레드-그린-리팩토링과 같은 잘못된 방법을 설파한다.
Red (실패): 구현하고자 하는 기능의 테스트 코드를 먼저 작성한다. 테스트는 실패해야 한다.
Green (통과): 실패한 테스트를 통과할 만큼의 최소한의 실제 코드를 작성한다.
Refactor (리팩토링): 구현된 코드를 정리하고 최적화한다. 테스트는 통과 상태를 유지해야 한다.
이 방법은 완벽하게 개소리이고 이 방법론 때문에 개발자에게 잘못된 면죄부를 주게 된다.
무엇을 테스트 할 지 이해하지 못한 상태에서 실패하는 테스트를 먼저 작성하라는 건 무모한 용기를 주게된다.
통과하는 테스트로 개선하므로 옳게 테스트 했다는 자만심을 심어주고
리팩터링 할 때 테스트가 통과했으니 코드 품질이 높아졌다는 착각을 준다.
무엇을 테스트 할 지 모르는 상태에서 테스트 본질 자체를 이해하지 못하고 결국엔 BDD 형식, TDD, 코드 품질등의 단어에 집착하는 광신도가 된다.
앞선 글에서 계약에 대해 살펴보고 테스트는 계약의 검증서란걸 말했다.
다시 한 번 말하겠다.
테스트는 계약을 검증하는 살아있는 검증문서이다.
당연히 테스트는 계약의 검증문서이므로 계약을 테스트 해야한다.
계약은 어떤게 있고 어떻게 작성하는지는 뒷 시리즈에서 설명하겠다.
우선 간단한 계약사항을 가져왔다
계약
1. 총액은 모든 라인아이템의 단가 * 수량을 합한 값이어야 한다.
2. 단가 < 0 금지, 수량 <= 0 금지. 위반 시 반드시 예외를 던져야 한다.
3. 계산은 반드시 정수로 한다. (원 단위)
계약이 잘 정의되고 정제되어 있으면 당연히 무엇을 검증할지 너무도 명백하다
@Test
@DisplayName("검증 1) total == Σ(단가 * 수량)")
void total_equals_sum_of_unitPrice_times_quantity() {
// (계약 구현체 준비)
계약구현체 구현체1 = new 계약구현체();
// (계약값 준비) 정상 라인아이템들
라인아이템들 계약값 = 라인아이템들.of(
라인아이템.of(단가=1000, 수량=2), // 2000
라인아이템.of(단가=500, 수량=3) // 1500
);
// (검증) total == 3500
assertEquals(3500, 구현체1.total(계약값));
}
@Test
@DisplayName("검증 2) 단가 < 0 이면 예외")
void throws_when_unitPrice_is_negative() {
// (계약 구현체 준비)
계약구현체 구현체1 = new 계약구현체();
// (계약값 준비) 단가가 음수인 라인아이템 포함
라인아이템들 계약값 = 라인아이템들.of(
라인아이템.of(단가=-1, 수량=1)
);
// (검증) 계약 위반 -> 반드시 예외
assertThrows(예외.class, () -> 구현체1.total(계약값));
}
@Test
@DisplayName("검증 3) 수량 <= 0 이면 예외")
void throws_when_quantity_is_zero_or_negative() {
// (계약 구현체 준비)
계약구현체 구현체1 = new 계약구현체();
// (계약값 준비) 수량 0
라인아이템들 계약값1 = 라인아이템들.of(
라인아이템.of(단가=1000, 수량=0)
);
// (검증) 계약 위반 -> 반드시 예외
assertThrows(예외.class, () -> 구현체1.total(계약값1));
// (계약값 준비) 수량 음수
라인아이템들 계약값2 = 라인아이템들.of(
라인아이템.of(단가=1000, 수량=-1)
);
// (검증) 계약 위반 -> 반드시 예외
assertThrows(예외.class, () -> 구현체1.total(계약값2));
}
@Test
@DisplayName("검증 4) 정수가 아닌 계산이면 예외")
void throws_when_non_integer_calculation_is_attempted() {
// (계약 구현체 준비)
계약구현체 구현체1 = new 계약구현체();
// (계약값 준비) 소수 단가(원 단위 위반)
라인아이템들 계약값 = 라인아이템들.of(
라인아이템.of(단가=10.5, 수량=2)
);
// (검증) 정수 계약 위반 -> 반드시 예외
assertThrows(예외.class, () -> 구현체1.total(계약값));
}
위의 예제에서 테스트 예제를 적었는데 3가지 포인트가 있다
Displayname는 검증사항을 적는다. -> 테스트가 검증 문서이므로 명시적으로 검증사항을 적어야 한다.
assert문이 가장 중요하다.
그 위에는 setup이고 구현 부산물이라 테크닉적인 영역이다. 의미는 거의 없다
테스트를 만든 개발자들이 assert라고 대놓고 박아놨다.
assert의 뜻은 단언하다라는 뜻이다. 무엇을 단언하는 걸까?
구현체는 /단언하다 /계약사항을 만족함을
assert문이 있다는 것 자체가 테스트는 검증문서 역할을 해야한다는 증거이다.
테스트에서 assert문이 가장 중요하고 나머지 위 코드는 setup의 과정이다.
검증문을 작성하기 위해선 계약값, 구현체 값 2개가 필요하다. setup과정은 이 2가지를 만드는데 의미가 있다.
테스트가 어려운 이유는 무엇을 테스트 할지 막막해서이다.
우리가 계약 사항을 명시적으로 설계하고 작성한다면 무엇을 검증할 지 너무도 명확해진다.
무엇을 검증할 지 명확해지면 assert문을 작성하기 너무 쉬워지고 setup의 과정은 귀찮은 영역일 뿐 테스트를 쉽게 작성할 수 있다.