2021. 8. 11. 15:28ㆍJAVA/JUNIT
본 글은 자바와 JUNIT을 활용한 실용주의 단위 테스트 도서의 내용을 복습하기 위해 작성된 글입니다.
글의 목적
- 실전에 가까운 코드를 분석
- 코드에 대한 경로를 커버하는 1~2개의 테스트 코드 구현 및 실습
- 테스트 배치에 대한 준비-실행-단언(AAA, Arrange-Act-Assert) 구조 분석
- 테스트 메서드의 공통된 로직을 모을 수 있는 @Before 애노테이션 분석 및 실습
1. 테스트 대상 이해: Profile 클래스
iloveyouboss 애플리케이션의 일부에 대한 테스트를 작성합니다. 이 애플리케이션은 잠재적인 구인자에게 유망한 구직자를 매칭하고 데이트 웹 사이트가 그러하듯 반대 방향에 대한 서비스도 제공합니다.
구인자와 구직자는 둘 다 다수의 객관식 혹은 yes-no 질문에 대한 대답을 하는 프로파일을 생성합니다. 웹 사이트는 다른 측 기준에 맞는 프로파일로 점수를 매기고, 고용주와 고용자 모두의 관점에서 최상의 매치를 보여줍니다.
iloveyouboss의 핵심 클래스인 Profile 클래스
package iloveyouboss;
import java.util.*;
// 어떤 사람이 회사 혹은 구직자에게 물어볼 수 있는 적절한 질문에 대한 답변을 담고 있음
public class Profile {
private Map<String,Answer> answers = new HashMap<>(); // key : 질문, value : 대답(Answer 객체)
private int score; // 점수
private String name; // 회사 혹은 구직자
public Profile(String name) {
this.name = name;
}
public String getName() {
return name;
}
// Answer 객체 : 질문에 대한 true 값을 갖는 객체
// add() : Answer 객체를 Profile에 추가합니다.
public void add(Answer answer) {
// 질문에 대한 답을 저장한다.
answers.put(answer.getQuestionText(), answer);
}
// Criteria : 다수의 Criterion 객체를 담는 컨테이너
// Criterion : 고용주가 구직자를 찾거나 그 반대를 의미, Answer 객체와 그 질문에 대한 가중치를 의미하는 Weight 객체를 캡슐화합니다.
// matches() : Criteria 객체를 인자로 받아 각 Criterion에
// 대해 반복문을 실행하여 해당 기준이 프로파일에 있는 답변과 맞는지 결정합니다.
// 기준이 절대적이지만 정답과 맞지 않는다면 matches() 메서드는 false를
// 반환합니다. 그리고 프로파일에 맞는 기준이 없다면 flase를 반환합니다.
// 그 외의 경우에는 true를 반환합니다.
public boolean matches(Criteria criteria) {
score = 0;
boolean kill = false;
boolean anyMatches = false;
for (Criterion criterion: criteria) {
// Answer 객체는 Question 객체를 참조하고 그 대답에 대한 적절한 값을 포합합니다.
Answer answer = answers.get(
criterion.getAnswer().getQuestionText());
boolean match =
criterion.getWeight() == Weight.DontCare ||
answer.match(criterion.getAnswer());
if (!match && criterion.getWeight() == Weight.MustMatch) {
kill = true;
}
if (match) {
score += criterion.getWeight().getValue();
}
anyMatches |= match;
}
if (kill)
return false;
return anyMatches;
}
public int score() {
return score;
}
}
2. 어떤 테스트를 작성할 수 있는지 결정
일부 복잡한 메서드에서는 테스트 코드를 작게는 수십 개 혹은 수백 개를 작성할 수도 있습니다. 코드에서 분기점이나 잠재적으로 영향력이 큰 데이터 변형들도 고려해 볼 수 있습니다. 시작점은 반복문, if 문과 복잡한 조건문들을 보는 것입니다. 그 후 데이터가 null이거나 0인 경우와 같은 데이터 변형들도 고려합니다.
Profile 클래스의 matches 메서드 테스트 케이스 고려사항
- Criteria 인스턴스가 Criterion 객체를 포함하지 않을 때
- Criteria 인스터가 다수의 Criterion 객체를 포함할 때
for (Criterion criterion: criteria) {
- answer.get()에서 반환된 Answer 객체가 null일 때
- criterion.getAnswer() 혹은 criterion.getAnswer().getQuestionText()의 반환값이 null일 때
Answer answer = answers.get(criterion.getAnswer().getQuestionText());
- criterion.getWeight()의 반환값이 Weight.DontCare여서 match 변수가 true일 때 (true)
- 두 조건문이 모두 false여서 결과적으로 match 변수가 false가 될 때 (false)
boolean match = criterion.getWeight() == Weight.DontCare || answer.match(criterion.getAnswer());
- match 변수가 false이고 criterion.getWeight()가 Weight.MustMatch여서 kill 변수가 true일 때 (flase)
- match 변수가 true이기 때문에 kill 변수가 변하지 않을 때 (true)
- criterion.getWeight()가 Weight.MustMatch가 아니기 때문에 kill 변수가 변하지 않을 때 (true)
if (!match && criterion.getWeight() == Weight.MustMatch) {
kill = true;
}
- match 변수가 true이기 때문에 score 변수가 업데이트되었을 때 (true)
- match 변수가 false이기 때문에 score 변수가 업데이트되지 않았을 때 (false)
if (match) {
score += criterion.getWeight().getValue();
}
- kill 변수가 true이기 때문에 matches 메서드가 false를 반환할 때 (false)
- kill 변수가 false이고 anyMatches 변수가 true이기 때문에 matches 메서드가 true를 반환할 때 (true)
- kill 변수가 false이고 anyMatches 변수가 false이기 때문에 matches 메서드가 false를 반환할 때 (false)
if (kill)
return false;
return anyMatches;
위의 조건 목록 외에도 더 좋은 테스트 케이스 고려사항이 존재할 수 있습니다.
3. 단일 경로 커버
matches() 메서드에서 중요한 로직은 for 반복문 안에 존재합니다. 반복문을 따라 한 가지 경로를 커버하는 단순한 테스트를 실습하겠습니다.
@Test
public void matchAnswersFalseWhenMustMatchCriteriaNotMet() {
Profile profile = new Profile("Bull Hockey, Inc.");
Question question = new BooleanQuestion(1, "Got bonuses?");
Criteria criteria = new Criteria();
Answer profileAnswer = new Answer(question, Bool.FALSE);
profile.add(profileAnswer);
Answer criteriaAnswer = new Answer(question, Bool.TRUE);
Criterion criterion = new Criterion(criteriaAnswer, Weight.MustMatch);
criteria.add(criterion);
boolean matches = profile.matches(criteria);
assertFalse(matches);
}
Profile을 생성한 후에는 질문을 생성합니다. 그리고 Criterion 객체를 추가하기 위해 Criteria 객체를 생성합니다.
Profile profile = new Profile("Bull Hockey, Inc.");
Question question = new BooleanQuestion(1, "Got bonuses?");
Criteria criteria = new Criteria();
질문(question)에 대한 답을 설정(Bool.FALSE)합니다. 그리고 답을 담은 객체(profileAnswer)를 profile에 추가합니다.
Answer profileAnswer = new Answer(question, Bool.FALSE);
profile.add(profileAnswer);
Criterion 객체를 생성하여 답변(Answer)과 그 가중치(Weight.MustMatch)를 저장합니다.
Answer criteriaAnswer = new Answer(question, Bool.TRUE);
Criterion criterion = new Criterion(criteriaAnswer, Weight.MustMatch);
criteria.add(criterion);
profile과 criteria객체를 매치하여 매치 결과를 저장합니다. 그리고 매치 결과로 False라고 단언합니다.
boolean matches = profile.matches(criteria);
assertFalse(matches);
실행결과
상여를 받았나요?(Got bonuses?)에 대한 답변으로 profile에는 Flase로 답변하고 criterion에는 True로 기준을 설정하였기 때문에 profile과 criterion 매치 결과 False가 나오게 되고 aseertFalse로 False라고 단언하였으므로 테스트 결과 성공으로 나오게 되었다.
4. 두 번째 테스트 생성하기
두 번째 테스트는 match 지역 변수에 대한 할당을 실습해봅니다. 기준 가중치가 DontCare이면 match 변수는 true가 됩니다. 메서드의 나머지 코드에서는 단일 기준이 true이면 matches() 메서드는 true를 반환합니다.
@Test
public void matchAnswersTrueForAnyDontCareCriteria() {
// 준비
Profile profile = new Profile("Bull Hockey, Inc.");
Question question = new BooleanQuestion(1, "Got milk?");
Criteria criteria = new Criteria();
Answer profileAnswer = new Answer(question, Bool.FALSE);
profile.add(profileAnswer);
Answer criteriaAnswer = new Answer(question, Bool.TRUE);
Criterion criterion = new Criterion(criteriaAnswer, Weight.DontCare);
criteria.add(criterion);
// 실행
boolean matches = profile.matches(criteria);
// 단언
assertTrue(matches);
}
질문(Got milk?)에 대한 profile 답변은 False이지만 기준(criterion) 설정시 가중치 값을 DontCare로 설정하였으므로 profile과 criterion 매치시 true가 나오게 되고 단언에서도 True라고 단언하게 된다.
실행결과
실행결과 True로 단언한 것이 일치한 것을 확인할 수 있습니다.
문제점
위의 테스트 코드에서 matchAnswersFalseWhenMustMatchCriteriaNotMet() 메서드와 matchAnswersTrueForAnyDontCareCriteria() 메서드는 상당히 유사한 것을 확인할 수 있습니다. 이러한 유사한 점은 테스트 메서드가 증가할때마다 비효율적이라고 생각합니다. 따라서 이 문제를 해결하기 위해서는 @Before 메서드로 해결할 수 있을 것입니다.
5. @Before 메서드로 테스트 초기화
주목할 점은 ProfileTest 클래스의 모든 테스트 코드에 포함되어 있는 공통적인 초기화 코드입니다. 위와 같이 테스트 두 개가 중복된 로직을 가지고 있다면 @Before 메서드로 이동할 수 있습니다. 각각의 JUnit 테스트는 실행할 때마다 @Before 애너테이션으로 표시된 메서드를 먼저 실행하게 될 것입니다.
public class ProfileTest {
private Profile profile;
private BooleanQuestion question;
private Criteria criteria;
// @Befroe 애노테이션 적용
// @Test 애노테이션이 설정된 테스트 메서드들의 공통된 로직
@Before
public void create() {
System.out.println("call create");
profile = new Profile("Bull Hockey, Inc.");
question = new BooleanQuestion(1, "Got bonuses?");
criteria = new Criteria();
}
// 테스트 코드 1, @Before 애노테이션 적용 및 지역변수 인라인 적용 after
@Test
public void matchAnswersFalseWhenMustMatchCriteriaNotMet() {
profile.add(new Answer(question, Bool.FALSE));
criteria.add(new Criterion(new Answer(question, Bool.TRUE), Weight.MustMatch));
boolean matches = profile.matches(criteria);
assertFalse(matches);
}
// 테스트 코드 2, @Before 애노테이션 적용 및 지역변수 인라인 적용 after
@Test
public void matchAnswersTrueForAnyDontCareCriteria() {
// 준비
profile.add(new Answer(question, Bool.FALSE));
criteria.add(new Criterion(new Answer(question, Bool.TRUE), Weight.DontCare));
// 실행
boolean matches = profile.matches(criteria);
// 단언
assertTrue(matches);
}
}
실행결과
실행결과, 이전 테스트 코드 실행결과와 동일한 것을 확인할 수 있습니다.
References
실습 소스코드, https://github.com/yonghwankim-dev/JUNIT-study/tree/main/iloveyouboss_06
자바와 JUNIT을 활용한 실용주의 단위 테스트
'JAVA > JUNIT' 카테고리의 다른 글
AssertJ 테스트시 객체의 동등성(Equality) 검증 방법 (0) | 2024.04.07 |
---|---|
3.2 예외를 기대하는 세 가지 방법 (0) | 2021.08.13 |
3.1 JUnit 단언 (0) | 2021.08.13 |
2.1 JUNIT 테스트 고려사항 구현 (0) | 2021.08.11 |
1. 첫번째 JUnit 테스트 만들기 (0) | 2021.08.10 |