매달 비용을 지불해야 사용할 수 있는 유료 서비스가 있다. 이 서비스는 다음 규칙에 따라 서비스 만료일을 결정한다.
- 서비스를 사용하려면 매달 1만원을 선불로 납부한다. 납부일 기준으로 한 달 뒤가 서비스 만료일이 된다.
- 2개월 이상 요금을 납부할 수 있다.
- 10만원을 납부하면 서비스를 1년 제공한다.
납부한 금액 기준으로 서비스 만료일을 계산하는 기능을 TDD로 구현한다면 어떤 순서로 진행해야 할까? 먼저 테스트 클래스 이름을 정하자. 클래스 이름은 ExpiryDateCalculatorTest 로 정했다.
public class ExpiryDateCalculatorTest {}
쉬운 것부터 테스트
이제 테스트 메서드를 추가한다. 테스트를 추가할 때에는 다음 두 가지를 고려해야 한다.
- 구현하기 쉬운 것부터 먼저 테스트
- 예외 상황을 먼저 테스트
1만원 납부 시 한 달 뒤 같은 날을 만료일로 계산하는 것이 가장 쉬워보인다. 계산에 필요한 값은 납부일, 납부액이고 리턴할 결과는 계산된 만료일이다.
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
public class ExpiryDateCalculatorTest {
@Test
void payManWonThenExpireIsOneMonth(){
LocalDate billingDate = LocalDate.of(2019, 3, 1);
int payAmount = 10_000;
ExpiryDateCalculator cal = new ExpiryDateCalculator();
LocalDate expireDate = cal.calculateExpireDate(billingDate, payAmount); //payAmount == 10000 이면 billingDate를 1달 뒤로 업데이트해서 리턴해줌
assertEquals(LocalDate.of(2019, 4, 1), expireDate); //한달 뒤 날짜와 리턴받은 날짜가 같은지 확인
}
}
이 테스트를 통과시키려면 ExpiryDateCalculator 클래스를 만들고, calculateExpireDate() 메서드가 2019,04,01에 해당하는 LocalDate 값을 리턴하면 된다.
import java.time.LocalDate;
public class ExpiryDateCalculator {
public LocalDate calculateExpireDate(LocalDate billingDate, int payAmount){
return LocalDate.of(2019, 4, 1);
}
}
예를 추가하면서 구현을 일반화
이제 동일 조건의 예를 추가하면서 구현을 일반화해보자. 먼저 1만원을 납부하는 예를 하나 더 추가하고, 날짜만 2019,05,05를 납부일로 사용한다. 따라서 만료일은 2019,06,05가 되어야 한다. 이를 위한 테스트 코드를 추가했다.
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
public class ExpiryDateCalculatorTest {
@Test
void payManWonThenExpireIsOneMonth(){
LocalDate billingDate = LocalDate.of(2019, 3, 1);
int payAmount = 10_000;
ExpiryDateCalculator cal = new ExpiryDateCalculator();
LocalDate expireDate = cal.calculateExpireDate(billingDate, payAmount); //payAmount == 10000 이면 billingDate를 1달 뒤로 업데이트해서 리턴해줌
assertEquals(LocalDate.of(2019, 4, 1), expireDate); //한달 뒤 날짜와 리턴받은 날짜가 같은지 확인
LocalDate billingDate2 = LocalDate.of(2019, 5, 5);
int payAmount2 = 10_000;
ExpiryDateCalculator cal2 = new ExpiryDateCalculator();
LocalDate expireDate2 = cal2.calculateExpireDate(billingDate2, payAmount2);
assertEquals(LocalDate.of(2019, 6, 5), expireDate2);
}
}
이 테스트를 실행하면 다음과 같이 검증이 실패한다.
이제 구현을 고민할 차례다. 통과시키기 위해 한번 더 상수를 사용할까? 아니면 바로 구현을 일반화할까? 이 예는 비교적 단순하므로 바로 구현해도 될 것 같다.
import java.time.LocalDate;
public class ExpiryDateCalculator {
public LocalDate calculateExpireDate(LocalDate billingDate, int payAmount){
return billingDate.plusMonths(1);
}
}
LocalDate 클래스의 plusMonth() 메소드를 사용해 1달을 더하는 방식의 코드를 구현했다. 테스트는 통과한다.
코드 정리 : 중복 제거
리팩토링할 시간이다. ExpireDateCalculator 클래스부터 보자. calculateExpireDate() 메서드는 파라미터가 두 개이다. 아직은! 파라미터가 더 많으면 객체 형태로 바꿔서 파라미터를 한 개로 만들겠지만, 아직 파라미터가 더 추가될지 알 수 없다. 발생하지도 않았는데 미리 단정 지어 코드를 수정할 필요는 없다.
테스트에 정리할 코드는 없을까? 테스트 메서드에는 중복이 존재한다.
LocalDate billingDate2 = LocalDate.of(2019, 5, 5);
위 코드 밑으로 중복이 된다. 보통은 중복을 제거하는 게 좋지만, 테스트 코드의 중복 제거는 고민이 필요하다. 각 테스트 메서드는 스스로 무엇을 테스트하는지 명확히 설명할 수 있어야 하기 때문이다. 테스트 코드의 구현 중복을 기계적으로 제거하면 자칫 메서드가 검증하고 싶은 내용을 알아보기 힘들 수 있다.
일단 중복 제거를 해보고 테스트 코드가 여전히 자신을 설명하고 있는지 확인해보자. 메서드를 이용해서 중복을 제거한 결과는 다음과 같다.
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
public class ExpiryDateCalculatorTest {
@Test
void payManWonThenExpireIsOneMonth(){
assertExpiryDate(LocalDate.of(2019, 3, 1), 10_000, LocalDate.of(2019, 4, 1));
assertExpiryDate(LocalDate.of(2019,5, 5), 10_000, LocalDate.of(2019, 5, 5));
}
private void assertExpiryDate(LocalDate billingDate, int payAmount, LocalDate expectedExpiryDate){
ExpiryDateCalculator cal = new ExpiryDateCalculator();
LocalDate realExpireDate = cal.calculateExpireDate(billingDate, payAmount);
assertEquals(expectedExpiryDate, realExpireDate);
}
}
assertExpireDate() 메서드의 첫 번째 파라미터와 세 번째 파라미터가 LocalDate 탕비이다. 둘 중 어떤 파라미터가 납부일이고 어떤 파라미터가 기댓값인지 구분하려면 assertExpiryDate() 메서드의 구현을 찾아봐야 하는 복잡함이 있다. 그래도 이 메서드가 길지 않고 파라미터 개수도 세 개여서 테스트 코드를 보면 어떤 것을 검증하는지 쉽게 확인할 수는 있을 것 같다. 이 정도면 중복을 제거해도 될 거 같다.
물론 추가적으로 더 개선할 수는 있지만, 우선은 이정도 에서 만족하자. 아직 모든 구현을 끝낸 게 아니기 때문이다.
예외 상황 처리
쉬운 구현을 하나 했으니 이제 예외 상황을 찾아보자. 단순히 한 달 추가로 끝나지 않는 상황이 존재한다.
- 납부일이 2019-01-31이고 납부액이 1만원이면 만료일은 2019-02-28이다.
- 납부일이 2019-05-31이고 납부액이 1만원이면 만료일은 2019-06-30이다.
- 납부일이 2020-01-31이고 납부액이 1만원이면 만료일은 2020-02-29이다.
각각 다음 달이 28일 2월인 경우, 한 달이 31일인 월 다음 30일인 월인 경우, 윤년의 케이스이다.
이 세가지 조건은 납부일 기준으로 다음 달의 같은 날이 만료일이 아니다. 이를 테스트로 추가해야 한다. 먼저 첫 번째 케이스를 테스트로 추가하자.
@Test
void unequalBillingDateExpireDate(){
assertExpiryDate(LocalDate.of(2019, 1, 31), 10000, LocalDate.of(2019, 2, 28));
}
실행해보니 바로 통과한다! LocalDate.plusMonths() 메소드가 알아서 한달 추가 처리를 해 준 것 같다. 나머지 두 케이스도 바로 해보자.
@Test
void unequalBillingDateExpireDate(){
assertExpiryDate(LocalDate.of(2019, 1, 31), 10000, LocalDate.of(2019, 2, 28));
assertExpiryDate(LocalDate.of(2019, 5, 31), 10000, LocalDate.of(2019, 6, 30));
assertExpiryDate(LocalDate.of(2020, 1, 31), 10000, LocalDate.of(2020, 2, 29));
}
나머지 케이스도 바로 통과한다.
'Book > 테스트 주도 개발 시작하기' 카테고리의 다른 글
TDD - 테스트 코드 작성 순서 (2) (1) | 2023.12.20 |
---|---|
TDD - 테스트 코드 작성 순서 (1) (0) | 2023.12.20 |