// 1. Criteria 객체가 Criterion 객체를 하나도 포함하고 있지 않은 경우 // 2. Criteria 객체가 Criterion 객체를 2개 이상 포함하고 있는 경우 criteria.forEach { criterion -> // 3. answers에서 반환한 Answer 객체가 null인 경우 // 4. criterion.answer 혹은 criterion.answer.questionText 값이 null인 경우 var answer = answers.get(criterion.answer.questionText)
// 5. criterion.weight의 값이 Weight.DontCare 여서 match가 true로 초기화되는 경우 // 6. answer?.match(criterion.answer)이 true여서 match가 true로 초기화되는 경우 // 7. 5번과 6번 조건이 모두 false여서 match가 false로 초기화되는 경우 var match = criterion.weight == Weight.DontCare || answer?.match(criterion.answer) == true
// 8. match가 false이고 criterion.weight의 값이 Weight.MustMatch 여서 kill이 true로 초기화되는 경우 // 9. match가 true여서 kill이 false로 유지되는 경우 // 10. criterion.weight의 값이 Weight.MustMatch가 아니어서 kill이 false로 유지되는 경우 if (match.not() && criterion.weight == Weight.MustMatch) { kill = true } // 11. match의 값이 true여서 score 값이 업데이트 되는 경우 // 12. match의 값이 false여서 score 값이 업데이트 되지 않는 경우 if (match) { score += criterion.weight.value } anyMatches = anyMatches || match } // 13. kill의 값이 true여서 matches 메서드가 false를 반환하는 경우 if (kill) { returnfalse } // 14. anyMatches의 값이 true여서 matches 메서드가 true를 반환하는 경우 // 15. anyMatches의 값이 false여서 matches 메서드가 false를 반환하는 경우 return anyMatches }
funscore(): Int = score }
모든 분기점 및 데이터의 변경 포인트마다 주석으로 작성하였다.
총 15개의 경우의 수가 존재함을 알 수 있다.
그렇다면 15개의 경우의 수가 맞춰 테스트 코드도 15개를 작성해야할까?
자세히 살펴보면 특정 조건을 충족하는 경우 후위에 위치하는 경우가 종속되는 부분이 보일 것이다.
이렇게 종속적인 조건들은 하나의 테스트로 묶을 수 있다.
2.3. 단일 경로 커버하기
원제 : Covering One Path
Profile 클래스의 maches() 메서드의 복잡도는 어디가 제일 높을까?
대부분 반복문을 선택할 것이다. 이 반복문을 따라 한 가지 경로를 커버하는 테스트 코드를 작성해보자.
먼저 준비물로는 뭐가 필요할지 고민해보자.
우선 Profile 객체를 하나 생성해야할 것이고, matches() 메서드에 파라미터로 넘겨줄 Criteria 객체가 필요하다.
이제 테스트 코드를 준비해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
classProfileTest {
@Test funtest() { val profile = Profile("Bull Hockey, Inc.") val question: Question = BooleanQuestion(1, "Got bonuses?") val profileAnswer = Answer(question, false) profile.add(profileAnswer) val criteria = Criteria() val criteriaAnswer = Answer(question, true) val criterion = Criterion(criteriaAnswer, Weight.MustMatch)
criteria.add(criterion) } }
테스트를 할 준비를 마쳤다.
이제 실행과 검증 로직을 추가해보고, test() 도 좀 더 적절한 이름으로 변경해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
classProfileTest {
@Test funmatchAnswersFalseWhenMustMatchCriteriaNotMet() { val profile = Profile("Bull Hockey, Inc.") val question: Question = BooleanQuestion(1, "Got bonuses?") val profileAnswer = Answer(question, false) profile.add(profileAnswer)
val criteria = Criteria() val criteriaAnswer = Answer(question, true) val criterion = Criterion(criteriaAnswer, Weight.MustMatch) criteria.add(criterion)
val matches = profile.matches(criteria) assertFalse(matches) } }
테스트 코드를 통해 검증하고자 하는 의미를 강조하기 위해 메서드명을 교체하였다.
교체한 메서드명을 통해 사용자가 원하는 매칭 결과가 아닐 경우 실패가 나와야 정상인 케이스를 검증할 수 있음을 유추할 수 있다.
여기서 한 번 더 개선해보자.
테스트 코트는 총 10줄로 그다지 길지 않게 느껴지지만, 모든 조건을 동일한 방식으로 검증하면 150줄의 테스트 코드를 작성해야할 것이다.
기존 코드의 길이보다 테스트 코드의 길이가 너무 커지는 것은 과도한 유지보수 비용이 필요할 수도 있다.
@Test funmatchAnswersTrueForAnyDontCareCriteria() { val profile = Profile("Bull Hockey, Inc.") val question: Question = BooleanQuestion(1, "Got milk?") val profileAnswer = Answer(question, false) profile.add(profileAnswer)
val criteria = Criteria() val criteriaAnswer = Answer(question, true) val criterion = Criterion(criteriaAnswer, Weight.DontCare) criteria.add(criterion)
val matches = profile.matches(criteria)
assertTrue(matches) } }
처음으로 작성한 테스트 코드와 비교해보면 로직의 단 두 줄만 바뀐 것을 확인할 수 있다.
나머지 부분을 공통화하는 방법이 있다면 획기적으로 코드의 길이를 줄일 수 있을 것이다.
2.5. @BeforeEach 메서드 사용하기
원제 : Initializing Tests with @Before Methods
이제 테스트 코드의 공통 부분을 별도로 빼서 재사용하는 방법을 알아보자.
JUnit은 본격적인 테스트의 수행 전에 미리 초기화할 수 있는 방법을 제공한다.
@BeforeEach 어노테이션을 이용한 메서드를 작성하면 된다.
참고 JUnit 4는 @Before 이지만, JUnit 5부터 @BeforeEach를 사용해야 한다.