외부 라이브러리에에 의존하는 서비스를 통합테스트 하고 있었습니다. 이때 발생했던 문제를 Mockito를 이용해 처리하는 과정을 정리해보도록 하겠습니다.
시나리오
- 해외에서 여행을 온 관광객이 물건을 구매하고, 환급을 요청합니다.
- 요청된 환급을 관리자가 검증합니다.
- 검증이 완료되면, 사용자의 MobileApp에 Push알림이 전송됩니다.
테스트를 진행할 도메인은 위와 같습니다.
테스트할 메소드
xxxxxxxxxx
public class AdminRefundEstimateService {
// ...
private final FirebasePushService pushService;
public void estimateRefundRequest(Long refundId, AdminRefundEstimateRequest request, byte[] refundResultBarcodeImageBytes) {
// 환급 처리 비즈니스 로직 (생략)
// push 알림
pushService.noti(new PushNotification(refundEntity.getRequestorId(), "환급평가 완료", "환급 평가가 완료되었습니다."));
}
}
테스트를 하고 싶은 메소드는 사용자가 요청한 환급을 검증하는 관리자의 API입니다. 벌써 큰 문제가 보입니다. 바로 해당 메소드가 외부 서비스에 의존하고 있는 것입니다.
문제점
환급처리 후 사용자에게 알림을 주기 위해, Firebase를 이용합니다. 이때 알림을 주기위해, 실제 동작하는 Push 토큰
이 발행되어야합니다. 그렇지 않은 경우, FirebasePushService에서 Exception을 발생시킵니다.
이때의 문제점은 아래와 같습니다.
테스트 속도가 느려진다
- FireBase에서 요청을 처리하고 사용자에게 알림을 Push한 다음 응답이 오기까지 기다려야 합니다. 이는 매우 비효율적인 테스트입니다.
독립적인 테스트가 어렵다
- 우리가 테스트할 대상은 환급을 검증하는 것입니다. 하지만, Push 알림에 의존하고 있기 때문에 환급처리만을 검증하기가 어렵습니다.
해결 방법
1. Spring의 @Async와, ApplicationEventPublisher를 통해 해결해보자
우선 이 글에서 궁극적으로 설명드릴 해결방법인 mockito를 사용하기 이전, Spring에서 제공하는 Event를 이용해 문제를 해결하려 시도해보겠습니다.
xxxxxxxxxx
public class PushNotificationEventHandler {
private final FirebaseCloudMessageService service;
public void sendPushNotification(PushNotificationEvent event) {
service.sendMessageTo(event);
}
}
public class AdminRefundEstimateService {
// ...
private final ApplicationEventPublisher eventPublisher;
public void estimateRefundRequest(Long refundId, AdminRefundEstimateRequest request, byte[] refundResultBarcodeImageBytes) {
// 환급 처리 비즈니스 로직 (생략)
// push 알림 이벤트 (Async로 동작)
eventPublisher.publishEvent(new PushNotificationEvent(refundEntity.getRequestorId(), "환급평가 완료", "환급 평가가 완료되었습니다."));
}
}
위의 eventPublisher에 의해 구동될 EventListener
는 Async로 동작하는데요, 이 때문에, 테스트시, Push 알림에 대한 테스트를 진행하지 않을 수 있게 됩니다.
2. Mockito를 이용한 해결
1.
에서 살펴본 방법은 매우 위험한 해결방법입니다. 왜냐하면, 임시적으로는 해결되었을지는 몰라도 아직 테스트가, 테스트 대상이 아닌 서비스의 구현에 의존하고 있기 때문입니다.
이럴 일은 거의 없겠지만, 만약 Push 알림이 동기적으로 처리되도록 변경된다면, 테스트가 실패하게 됩니다.
x
public class AdminRefundEstimateServiceTest {
FirebaseCloudMessageService service;
public void init() {
doNothing().when(service).sendMessageTo(any(PushNotificationEvent.class));
}
public void 환급요청평가() throws Exception {
//given, when, then
}
}
이러한 경우, mockito
를 사용하면 됩니다. @MockBean
을 이용해 가짜 객체를 주입받고, doNothing()
을 이용해 해당 서비스의 처리방법을 모킹합니다. 이때, 파라미터값은 상관이 없기 때문에, any(Event.class)
를 이용해 가짜 파라미터를 주입합니다.
중점 포인트
우리가 mockito를 이용해 위와 같이 처리할 수 있는 이유는 아래와 같습니다
- Push 알림 성공 여부는 admin 환급 처리와 상관이 없다.
더 나아가서
사실, Push 알림 요청을 비즈니스 로직에서 요청하지 않는다면, mockito를 이용할 필요도 없습니다. 이후의 포스팅에서는 aop를 이용해 event 요청을 weaving하여, 비즈니스 로직과 완전히 분리시켜볼 것입니다.
+ aop를 이용한 push알림과 비즈니스 로직 분리 - https://galid1.tistory.com/752
'FrameWork > Spring Boot' 카테고리의 다른 글
Spring Boot - REST API 인증 - 2 (Refresh Token이란?) (0) | 2020.07.03 |
---|---|
Spring Boot - 개발자를 기억하게하지 말자 (파일리스트와 데이터리스트 요청을 하나의 객체로 바인딩하기 : @ModelAttribute) (5) | 2020.07.01 |
Spring Boot - FCM Push 서버 구축하기 (15) | 2020.06.12 |
Spring Boot - rest docs 사용방법과 자동 목차생성(spring boot restdocs 설정) (2) | 2020.06.03 |
Spring Boot - 스프링 부트 통합테스트 방법과 팁(Spring boot Integration Test) (2) | 2020.05.29 |