JAVA 테스트 코드 작성 - 1. Mock 사용하기(TIL)
해당 게시글은 최범균님의 테스트 주도 개발 시작하기와 박우빈님의 실용적인 테스트 강의 내용 중 Mock 사용법을 정리한 내용입니다. 되게 테스트 상황에 맞는 예시를 생각해내기 어려워서 강의 내용, 개인 테스트 코드, 블로그에서 코드를 가져왔음을 알립니다.
1. Mock으로 기본 가짜 응답 만들기
DB와 연결된 부분이며 이미 기능적으로 검증이 완료된 부분은 Mock으로 가짜 객체를 만드는 것을 고려해보는 것이 좋다. 왜냐하면, 굉장히 시간이 오래 걸리며 데이터를 주입하고 관리하는 부분이 오히려 가독성을 해치고 관리하기 어렵게 만들기도 하기 때문이다.
과거 컨트롤러 테스트 글을 작성했을 때 요청에 따른 데이터 조회 부분을 Mock으로 대체해서 설정한 값에 따른 응답을 설정해놓은 적이 있었다.
// given
Shop shop = Shop.builder()
.name("학식")
.shopType(RESTAURANT)
.schoolType(NSC)
.price(8000)
.openTime(LocalTime.MIN)
.closeTime(LocalTime.MAX)
.build();
ShopSearchServiceRequest serviceRequest =
new ShopSearchServiceRequest(NSC, List.of(RESTAURANT), 5000, 10000, null);
given(shopService.findRandomShop(serviceRequest))
.willReturn(shop);
이렇게 given절을 구성하면 repository 의존성을 주입받고 데이터를 주입하고 테스트마다 그 내용을 지우는 과정을 작성하지 않아도 된다. 즉, 원하는 부분의 테스트하는데만 확실하게 집중할 수 있다.
추가적으로 저렇게 응답만 내보내는 것이 아닌 예외 상황도 내보낼 수 있다. 아래와 같이 특정한 값이 들어오면 예외를 내보내게 설정함으로써 예외 상황에 대한 테스트도 mock으로 진행할 수 있다.
@Test
void phoneBookSearchEx(){
// given
given(mockPhoneBookRepository.contains("example")).willThrow(new IllegalArgumentException());
// when // then
assertThrows(
IllegalArgumentException.class,
() -> phoneBookService.search("example"));
}
또한 BDDMockito 라이브러리에서 메서드를 선언할 때에 파라미터 값으로 타입에 맞게 어떤 값이 들어오건 상관없이 동작하도록 작성할 수 있다. 이런 경우는 특정한 값이 중요한 게 아니라 해당 응답이 중요한 경우에 사용하면 된다.
@Test
void phoneBookSearchEx(){
// given // "example"을 anyString()으로 변경!
given(mockPhoneBookRepository.contains(anyString())).willThrow(new IllegalArgumentException());
// when // then
assertThrows(
IllegalArgumentException.class,
() -> phoneBookService.search("example"));
}
이 때 주의할 점은 파라미터가 여러 개인 경우인데, 파라미터가 여러 개인 경우에는 Mockito에서 하나라도 any~~로 파라미터를 설정한 경우, 모든 인자를 any~~로 설정하게 해놓았다. 그래서 아래의 경우는 예외가 발생한다.
@Test
void phoneBookRegisterTest() {
// given
given(mockPhoneBookRepository.insert(anyString(), "phone")).willThrow(new IllegalArgumentException());
// when // then
assertThrows(
IllegalArgumentException.class,
() -> phoneBookService.register("name", "phone"));
}
차라리 두 가지 중 하나를 선택해서 바꾸어야 한다. 모든 파라미터를 any~~로 바꾸거나 아니면 argumentMatcher를 사용해주면 된다. 전자는 예상이 가니 적지 않도록하고 후자는 아래와 같다.
@Test
void phoneBookRegisterTest() {
// given
given(mockPhoneBookRepository.insert(anyString(), eq("phone"))).willThrow(new IllegalArgumentException());
// when // then
assertThrows(
IllegalArgumentException.class,
() -> phoneBookService.register("name", "phone"));
}
2. Mock 메서드 호출 여부 확인하기
Email 송수신 같은 외부 서버와 관련된 것들은 Mock을 통해서 행위 검증을 통해 체크를 해보는 것을 고려하는 게 좋다. 일단, 이들은 확인할 수 있더라도 시간이 오래 걸리고 통제할 수 없다. 정상적인 상황이라고 가정을 하고 해당 메서드가 제대로 호출되었는지 확인함으로써 테스트하는 걸 고려해보아야 한다.
예를 들어 유저를 등록하는 서비스 내부에 등록된 유저에게 확인용 이메일을 보내는 내용이 있다고 생각을 해보자. 그러면 아래처럼 Email을 보내는 행위가 있을텐데 실제 이메일을 보내고 보내졌는지를 테스트를 할 때마다 시행하고 확인하는 것은 시간 낭비이다.
public boolean register(String id, String password) {
User savedUser = userRepository.save(id, password);
if (savedUser != null) {
emailSender.sendEmail();
return true;
}
return false;
}
이럴 때, Mock을 사용할 수 있다. Mock을 사용해서 emailSender를 Mock 개체로 바꿔치기 하고 아무 행동도 안하게 하면 된다. 단지 save된 데이터가 있을 때 해당 메서드가 동작했는지만 확인하면 된다. 이를 행위 검증이라고 하며 Mock 라이브러리를 이용하면 사용이 가능하다. 사용 방법은 아래와 같다.
아래처럼 emailSender를 mock으로 바꾸어 생성을 하고 원래 테스트하고자 했던 registerService의 register 메서드를 호출하면 내부의 emailSender의 sendEmail이 호출되었을 것이다. 이 때, Mockito 라이브러리에서 함수 호출 여부를 판단할 수 있게 지원을 해주기 때문에 확인이 가능하다. 함수 내부의 파라미터에 따라서도 확인이 가능하고 횟수까지도 확인할 수 있게 지원해주니 이를 통해 다양한 상황을 검증해볼 수 있다.
// Mock 개체 생성 및 주입
private EmailSender emailSender = mock(EmailSender.class);
private UserRepository userRepository = new UserRepository();
private RegisterService registerService = new RegisterService(emailSender, userRepository);
@Test
void sendEmail() {
// given
registerService.register("id", "pw");
// when // then
then(emailSender).should().sendEmail(); // 이메일 호출 여부 판단
}
3. Mock 메서드로 void 메서드 다루기
1번의 예시를 작성하는 과정에서 void타입의 메서드인 경우 BDDMockito가 타입이 적절하지 않다며 오류를 내보내는 것을 확인했다. void 타입의 메서드를 스터빙하는 경우는 조금은 다른 방법을 사용해주어야 한다. 해당 메서드 자체 테스트가 아니라 어떻게 아무것도 안하게 하거나 예외를 던지게 할까에 대해 초점을 맞춰줬으면 좋겠다.
아래의 예시는 아무것도 안하게 하고 호출 여부를 확인하는 방법이다.
@Test
void voidMethodTest() {
// given
doNothing().when(mockPhoneBookRepository).add(anyInt(), anyString());
mockPhoneBookRepository.add(0, "1");
verify(mockPhoneBookRepository, times(1)).add(0, "1");
}
아래의 예시는 예외를 발생하게 하는 방법이다. 일반 다른 반환타입이 있는 메서드와는 방법이 조금은 다르다.
@Test
void voidMethodTest2() {
doThrow(IllegalArgumentException.class).when(mockPhoneBookRepository).add(anyInt(), isNull());
assertThrows(IllegalArgumentException.class, () -> {
mockPhoneBookRepository.add(1, null);
});
}
추가적으로 argumentCaptor를 통해 스터빙 시 원하는 위치에 두고 해당 메서드가 호출될 때 의도했던 값이 들어오는지 여부를 아래처럼 확인할 수도 있다. void메서드를 만나더라도 위의 3가지 방법을 잘 기억해두자.
@Test
void argumentCaptor() {
ArgumentCaptor<String> valueCapture = ArgumentCaptor.forClass(String.class);
doNothing().when(mockPhoneBookRepository).add(any(Integer.class), valueCapture.capture());
mockPhoneBookRepository.add(0, "captured");
assertEquals("captured", valueCapture.getValue());
}
[ 참고 자료 ]
https://www.baeldung.com/bdd-mockito
https://www.baeldung.com/mockito-void-methods
테스트 주도 개발 시작하기 - 최범균
실용적인 테스트 강의 - 박우빈