org.opentest4j.AssertionFailedError: Expected :2019-03-30 Actual :2019-03-28
테스트를 통과할만큼만 구현해보도록 하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
classExpiryDateCalculator {
funcalculateExpiryDate( payData: PayData ): LocalDate { if (payData.firstBillingDate != null) { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(1) if (payData.firstBillingDate.dayOfMonth != candidateExp.dayOfMonth) { // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(payData.firstBillingDate.dayOfMonth) } }
return payData.billingDate.plusMonths(1) }
}
이제 모든 테스트가 통과한다.
이제 다음 예외 상황을 추가하자.
첫 납부일이 2019년 5월 31일이고, 만료되는 2019년 6월 30일에 1만원을 납부하면 다음 만료일은 2019년 7월 31일이다.
funcalculateExpiryDate( payData: PayData ): LocalDate { if (payData.firstBillingDate != null) { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(1) if (payData.firstBillingDate.dayOfMonth != candidateExp.dayOfMonth) { // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(payData.firstBillingDate.dayOfMonth) } }
return payData.billingDate.plusMonths(1) }
}
아래에서 한 달 뒤의 날짜를 연산할 때 상수 1을 사용하고 있다.
1
payData.billingDate.plusMonths(1)
이를 아래와 같이 변수로 변경하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
classExpiryDateCalculator {
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = 1 if (payData.firstBillingDate != null) { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) if (payData.firstBillingDate.dayOfMonth != candidateExp.dayOfMonth) { // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(payData.firstBillingDate.dayOfMonth) } }
org.opentest4j.AssertionFailedError: Expected :2019-05-01 Actual :2019-04-01
테스트 통과를 위해 납부 금액에 따라 몇 개월씩 더할지 동적으로 계산해서 변수에 넣어주도록 하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
classExpiryDateCalculator {
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = payData.payAmount / 10_000L if (payData.firstBillingDate != null) { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) if (payData.firstBillingDate.dayOfMonth != candidateExp.dayOfMonth) { // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(payData.firstBillingDate.dayOfMonth) } }
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = payData.payAmount / 10_000L if (payData.firstBillingDate != null) { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) if (payData.firstBillingDate.dayOfMonth != candidateExp.dayOfMonth) { if (YearMonth.from(candidateExp).lengthOfMonth() < payData.firstBillingDate.dayOfMonth) { // candidateExp이 가지고 있는 월의 마지막 날이, 첫 납부일의 일자보다 적은 경우 return candidateExp.withDayOfMonth(YearMonth.from(candidateExp).lengthOfMonth()) } // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(payData.firstBillingDate.dayOfMonth) } }
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = payData.payAmount / 10_000L if (payData.firstBillingDate != null) { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) if (payData.firstBillingDate.dayOfMonth != candidateExp.dayOfMonth) { if (YearMonth.from(candidateExp).lengthOfMonth() < payData.firstBillingDate.dayOfMonth) { // candidateExp이 가지고 있는 월의 마지막 날이, 첫 납부일의 일자보다 적은 경우 return candidateExp.withDayOfMonth(YearMonth.from(candidateExp).lengthOfMonth()) } // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(payData.firstBillingDate.dayOfMonth) } }
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = payData.payAmount / 10_000L if (payData.firstBillingDate != null) { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) if (payData.firstBillingDate.dayOfMonth != candidateExp.dayOfMonth) { val dayLengthOfCandidateMonth = YearMonth.from(candidateExp).lengthOfMonth() if (dayLengthOfCandidateMonth < payData.firstBillingDate.dayOfMonth) { // candidateExp이 가지고 있는 월의 마지막 날이, 첫 납부일의 일자보다 적은 경우 return candidateExp.withDayOfMonth(dayLengthOfCandidateMonth) } // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(payData.firstBillingDate.dayOfMonth) } }
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = payData.payAmount / 10_000L if (payData.firstBillingDate != null) { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) val dayOfFirstBillingDate = payData.firstBillingDate.dayOfMonth if (dayOfFirstBillingDate != candidateExp.dayOfMonth) { val dayLengthOfCandidateMonth = YearMonth.from(candidateExp).lengthOfMonth() if (dayLengthOfCandidateMonth < payData.firstBillingDate.dayOfMonth) { // candidateExp이 가지고 있는 월의 마지막 날이, 첫 납부일의 일자보다 적은 경우 return candidateExp.withDayOfMonth(dayLengthOfCandidateMonth) } // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(dayOfFirstBillingDate) } }
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = payData.payAmount / 10_000L if (payData.firstBillingDate != null) { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) val dayOfFirstBillingDate = payData.firstBillingDate.dayOfMonth if (dayOfFirstBillingDate != candidateExp.dayOfMonth) { val dayLengthOfCandidateMonth = YearMonth.from(candidateExp).lengthOfMonth() if (dayLengthOfCandidateMonth < payData.firstBillingDate.dayOfMonth) { // candidateExp이 가지고 있는 월의 마지막 날이, 첫 납부일의 일자보다 적은 경우 return candidateExp.withDayOfMonth(dayLengthOfCandidateMonth) } // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(dayOfFirstBillingDate) } else { return candidateExp } } else { return payData.billingDate.plusMonths(monthsToAdd) } } }
테스트를 통과한다면 첫 납부일에 해당하는 영역을 들어내서 별도의 메서드인 expiryDateUsingFirstBillingDate()로 분리한다.
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = payData.payAmount / 10_000L if (payData.firstBillingDate != null) { return expiryDateUsingFirstBillingDate(payData, monthsToAdd) } else { return payData.billingDate.plusMonths(monthsToAdd) } }
privatefunexpiryDateUsingFirstBillingDate( payData: PayData, monthsToAdd: Long, ): LocalDate { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) val dayOfFirstBillingDate = payData.firstBillingDate!!.dayOfMonth if (dayOfFirstBillingDate != candidateExp.dayOfMonth) { val dayLengthOfCandidateMonth = YearMonth.from(candidateExp).lengthOfMonth() if (dayLengthOfCandidateMonth < payData.firstBillingDate.dayOfMonth) { // candidateExp이 가지고 있는 월의 마지막 날이, 첫 납부일의 일자보다 적은 경우 return candidateExp.withDayOfMonth(dayLengthOfCandidateMonth) } // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(dayOfFirstBillingDate) } else { return candidateExp } } }
이제 핵심 비즈니스 로직인 calculateExpiryDate() 메서드가 다소 명확해졌으며, 테스트 코드도 전부 통과한다.
이 다음엔 몇 가지 보조 메서드를 추가해보자.
첫 납부일의 일자와, 예상 납부일의 일자를 비교하는 메서드를 isSameDayOfMonth()로 분리한 뒤 적용하면 아래와 같다.
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = payData.payAmount / 10_000L if (payData.firstBillingDate != null) { return expiryDateUsingFirstBillingDate(payData, monthsToAdd) } else { return payData.billingDate.plusMonths(monthsToAdd) } }
privatefunexpiryDateUsingFirstBillingDate( payData: PayData, monthsToAdd: Long, ): LocalDate { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) val dayOfFirstBillingDate = payData.firstBillingDate!!.dayOfMonth if (isSameDayOfMonth(payData.firstBillingDate, candidateExp).not()) { val dayLengthOfCandidateMonth = YearMonth.from(candidateExp).lengthOfMonth() if (dayLengthOfCandidateMonth < payData.firstBillingDate.dayOfMonth) { // candidateExp이 가지고 있는 월의 마지막 날이, 첫 납부일의 일자보다 적은 경우 return candidateExp.withDayOfMonth(dayLengthOfCandidateMonth) } // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(dayOfFirstBillingDate) } else { return candidateExp } }
funcalculateExpiryDate( payData: PayData ): LocalDate { var monthsToAdd: Long = payData.payAmount / 10_000L if (payData.firstBillingDate != null) { return expiryDateUsingFirstBillingDate(payData, monthsToAdd) } else { return payData.billingDate.plusMonths(monthsToAdd) } }
privatefunexpiryDateUsingFirstBillingDate( payData: PayData, monthsToAdd: Long, ): LocalDate { // 후보 만료일 계산 val candidateExp = payData.billingDate.plusMonths(monthsToAdd) val dayOfFirstBillingDate = payData.firstBillingDate!!.dayOfMonth if (isSameDayOfMonth(payData.firstBillingDate, candidateExp).not()) { val dayLengthOfCandidateMonth = lastDayOfMonth(candidateExp) if (dayLengthOfCandidateMonth < payData.firstBillingDate.dayOfMonth) { // candidateExp이 가지고 있는 월의 마지막 날이, 첫 납부일의 일자보다 적은 경우 return candidateExp.withDayOfMonth(dayLengthOfCandidateMonth) } // 첫 납부일의 일자와 후보 만료일의 일자가 다른 경우, 첫 납부일의 일자를 후보 만료일의 일자로 사용한다. return candidateExp.withDayOfMonth(dayOfFirstBillingDate) } else { return candidateExp } }