통합 테스트 문제점 및 Mockito를 통한 단위 테스트

통합테스트의 문제점

 

테스트 코드 작성시 통합 테스트로 작성하면 다음과 같은 이슈들에 직면하게 될 수 있다.

 

  • 비즈니스 로직 테스트 작성시 불필요한 세부사항(DB, 보안) 까지 알아야한다.
  • 테스트 코드 수행시간이 오래걸린다.
  • 테스트 하나를 수행하기 위해 의존되어있는 통합적인 모든 시나리오를 테스트 작성자가 고려해야한다.
  • 불필요한 데이터, 객체를 생성해야한다.

다음 코드를 살펴보자.

 

CouponQuizService.java

 

@Service
@RequiredArgsConstructor
public class CouponQuizService {

    private final CouponRepository couponRepository;
    private final CouponIssueRepository couponIssueRepository;

    @Transactional
    public boolean registerQuizCoupon(Integer userId, Integer couponId, String quizAnswer) {
        Coupon coupon = couponRepository.selectCouponBy(couponId);
        if (coupon.verifyQuizAnswer(quizAnswer)) {
            couponIssueRepository.issueCoupon(couponId, userId);
            return true;
        }
        return false;
    }

}

 

CouponIssueRepository.java

@Slf4j
@Repository
public class CouponIssueRepository {
    public void issueCoupon(Integer couponId, Integer userId) {
    	log.info("쿠폰 발급 완료!");
    }
}

 

CouponRepository.java

@Repository
public class CouponRepository {

    public Coupon selectCouponBy(Integer couponId) {
        return Coupon.from("커피");
    }
}

 

Coupon.java

public class Coupon {
    private String quizAnswer;

    public boolean verifyQuizAnswer(String answer) {
        return this.quizAnswer.equals(answer);
    }
    
    public static Coupon from(String quizAnswer) {
    	Coupon coupon = new Coupon();
        coupon.quizAnswer = quizAnswer;
        return coupon;
    }
}

 

 

시나리오는 다음과 같이 단순하다.

 

  • 사용자는 퀴즈 정답을 입력하여 쿠폰 발급을 요청한다.
  • 사용자가 입력한 정답이 실제 정답과 일치하면 쿠폰이 발급된다.

위의 내용을 검증하는 통합테스트를 작성해보자.

 

@SpringBootTest
public class CouponQuizServiceTest {

    @Autowired
    private CouponQuizService couponQuizService;

    @Test
    public void 퀴즈_쿠폰_발급() {
        //given
        Integer couponId = 100;
        Integer userId = 9999;
        String quizAnswer = "커피";

        //when
        boolean result = couponQuizService.registerQuizCoupon(userId, couponId, quizAnswer);
		
        //then
        Assert.assertTrue(result);
    }
}

 

 

가정을 조금 수정해보자.

쿠폰의 수 많은 정책이 추가되었고 쿠폰이 발급되기 이전에 정책에 대한 검증이 통과되어야 쿠폰이 발급될 수 있다고 가정하자.

 

정책을 검증하는 CouponPolicyService가 추가되고

 

@Slf4j
@Service
public class CouponPolicyService {

    public boolean validateFirstPolicy(Integer couponId) {
        log.info("첫번째 정책 검증 과정 수행완료!");
        return true;
    }

    public boolean validateSecondPolicy(Integer couponId) {
        log.info("두번째 정책 검증 과정 수행완료!");
        return true;
    }
}

 

CouponQuizService도 다음과 같이 수정된다.

 

@Service
@RequiredArgsConstructor
public class CouponQuizService {

    private final CouponRepository couponRepository;
    private final CouponIssueRepository couponIssueRepository;
    private final CouponPolicyService couponPolicyService;

    @Transactional
    public boolean registerQuizCoupon(Integer userId, Integer couponId, String quizAnswer) {
        if (couponPolicyService.validateFirstPolicy(couponId)
            && couponPolicyService.validateSecondPolicy(couponId)) {
            Coupon coupon = couponRepository.selectCouponBy(couponId);
            if (coupon.verifyQuizAnswer(quizAnswer)) {
                couponIssueRepository.issueCoupon(couponId, userId);
                return true;
            }
        }
        return false;
    }

}

 

 

여기서 통합테스트의 문제점이 드러나기 시작한다. 쿠폰 발급 테스트 즉, CouponQuizService의 registerQuizCoupon 메시지가 올바르게 수행하는지 테스트 하기위해 registerQuizCoupon 메시지 책임 뿐만이 아닌 CouponPolicyService의 verifyPolicy 메시지 책임까지 테스트를 수행하게된다.

 

registerQuizCoupon 메시지가 쿠폰을 발급 받기 위해 CouponPolicyService 뿐만이 기타 Service 들을 추가적으로 의존하게 되면 어떻게 될까?

 

개발자는 단순히 쿠폰 발급 테스트 하나만을 위해 CouponQuizService에 의존되는 모든 시나리오, 환경, 필요한 데이터를 고려하여 테스트를 작성해야할 것이다.

 

그러므로 우리에게 필요한것은 통합테스트가 아닌 단위 테스트이다.

 

 

 

단위 테스트로 변경하기

 

통합테스트를 단위 테스트로 작성해보자.

 

먼저 CouponQuizService의 registerQuizCoupon 메시지의 구현된 로직을 테스트 할 때 테스트에서 제외되어야 할 메시지들은 다음과 같다.

 

  • CouponRepository의 selectCouponBy 메시지
  • CouponIssueRepository의 issueCoupon 메시지
  • CouponPolicyService의 verifyFirstPolicy, verifySecondPolicy 메시지

 

registerQuizCoupon 메시지 전송시 위의 메시지들을 흉내낼 수 있는 Mock 객체를 통해 단위 테스트를 작성하면된다.

 

@SpringBootTest
public class CouponQuizServiceTest {
    @MockBean
    private CouponRepository couponRepository;
    @MockBean
    private CouponIssueRepository couponIssueRepository;
    @MockBean
    private CouponPolicyService couponPolicyService;
    @Autowired
    private CouponQuizService couponQuizService;

    @Test
    public void 퀴즈_쿠폰_발급() {
        //given
        when(couponRepository.selectCouponBy(any())).thenReturn(Coupon.from("커피"));
        doNothing().when(couponIssueRepository).issueCoupon(any(), any());
        when(couponPolicyService.validateFirstPolicy(any())).thenReturn(true);
        when(couponPolicyService.validateSecondPolicy(any())).thenReturn(true);
        
        //when
        boolean result = couponQuizService.registerQuizCoupon(userId, couponId, quizAnswer);

        //then
        Assert.assertTrue(result);
    }
}

 

 


Mock 객체란?

 

테스트하기에 앞서 실제 객체를 생성하기에 필요로하는 데이터와 시간이 많이 들거나 의존성이 가지 많은 나무 처럼 길게 뻗어져 있는 경우 Mock객체를 생성하여 해결할 수 있다. 즉, 실제 객체를 흉내내는 객체이다.

 

 

@MockBeen 은 Spring boot 1.4 부터 추가된 어노테이션으로 Mock 객체를 만들어주는데 사용된다.

 

@MockBeen이 선언되면 해당 클래스를 Mock 객체로 생성하여 CouponQuizService 의 필드에 주입된다.

실제 객체가 아닌 Mock 객체이므로 각 Mock 객체들이 메시지를 수신 받을 때 어떤 내용을 수행할 지 설정 해야한다.

 흉내내야(Mocking) 한다.

 

다음은 Mocking 설정이다.

 

 

selectCouponBy 메시지 전송시 실제 DB 테이블에서 쿠폰 정보를 조회하여 쿠폰 객체를 반환하는 것이 아닌

Coupon.from("커피") 코드로 생성된 객체를 반환하도록 mocking.

-> 사용자가 입력한 정답과 실제 정답이 일치하도록 가정.

 

 

issueCoupon 메시지 전송시 아무 동작도 수행하지 않도록 mocking.

 

 

validatePolicy 메시지 전송시 true를 반환하도록 mocking.

-> 모든 쿠폰 정책 검증을 통과했다고 가정.

 


테스트 수행 결과

 

 

 

결론

 

통합테스트 보다는 단위테스트를 기본적으로 작성하는것은 옳다.

그러나 Mock 객체를 사용하는것이 무조건 옳지는 않다고 생각한다. 테스트 작성시 Mock 객체를 만드는일 자체가 하나의 클래스에 너무 많은 의존성이 포함됐다는 증거다. Mock 객체를 생성할 정도로 의존성이 많다면 클래스의 책임을 더 세부적으로 분리하여 디자인 해볼것을 고려 해야 한다. 그렇게 되면 Mock 객체를 생성 할 필요도 없어지게 된다.

 

 

 

 

'📦 JUnit' 카테고리의 다른 글

[JUnit 4] MockitoJUnitRunner VS SpringRunner  (0) 2021.01.21