2. JUNIT 사용

2021. 8. 11. 15:28JAVA/JUNIT

본 글은 자바와 JUNIT을 활용한 실용주의 단위 테스트 도서의 내용을 복습하기 위해 작성된 글입니다.

글의 목적

  1. 실전에 가까운 코드를 분석
  2. 코드에 대한 경로를 커버하는 1~2개의 테스트 코드 구현 및 실습
  3. 테스트 배치에 대한 준비-실행-단언(AAA, Arrange-Act-Assert) 구조 분석
  4. 테스트 메서드의 공통된 로직을 모을 수 있는 @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을 활용한 실용주의 단위 테스트