티스토리 뷰
Spring을 제대로 이용하기 위해서는 Spring이 갖는 기본 개념과 철학을 이해해야 한다. 이번에는 Spring에 입문하는 사람들을 위해 Spring에 대해 이해할 수 있는 내용을 작성해보고자 한다.
1. 올바른 단위 테스트 작성 - 독립적인 테스트
[ 기존의 단위 테스트 - 의존적인 테스트 ]
얘기해보고자 하는 상황은 public 메소드가 다른 public 메소드를 이용하는 경우이다.
예를 들어 다음과 같은 메일 전송 서비스 클래스가 있다고 하자. sendMailIfRegisteredEmail 함수는 isEmailRegistered를 호출하여 사용하고 있다.
@Service
@RequiredArgsConstructor
public class MailService {
private final MailRepository mailRepository;
public boolean sendMailIfRegisteredEmail(final String email) {
if(isEmailRegistered(email)) {
return sendMail(email);
}
return false;
}
public boolean isEmailRegistered(final String email) {
return mailRepository.existsByEmail(email);
}
public boolean sendMail(String email) {
... 생략
}
}
위의 클래스에 대해 다음과 같은 테스트 코드를 작성할 수 있다.
@ExtendWith(MockitoExtension.class)
class MailServiceTest {
@InjectMocks
private MailService mailService;
@Mock
private MailRepository mailRepository;
private final String email = "mangkyu@email.com";
@DisplayName("")
@Test
void sendMailIfRegisteredEmailSuccess() {
// given
doReturn(true).when(mailRepository).existsByEmail(email);
// when
final boolean result = mailService.sendMailIfRegisteredEmail(email);
// then
assertThat(result).isTrue();
// verify
verify(mailService, times(1)).sendMail(email);
}
@DisplayName("")
@Test
void sendMailIfRegisteredEmailFail_EmailNotRegistered() {
// given
doReturn(false).when(mailRepository).existsByEmail(email);
// when
final boolean result = mailService.sendMailIfRegisteredEmail(email);
// then
assertThat(result).isFalse();
// verify
verify(mailService, times(0)).sendMail(email);
}
@DisplayName("")
@Test
void isEmailRegisteredSuccess() {
// given
doReturn(true).when(mailRepository).existsByEmail(email);
// when
final boolean result = mailService.sendMailIfRegisteredEmail(email);
// then
assertThat(result).isFalse();
}
}
위의 테스트 코드는 큰 문제가 없어 보인다. 하지만 곰곰이 살펴보면 sendMailIfRegisteredEmail 에 대한 테스트 코드가 isEmailRegisteredSuccess와 상당히 중복되는 것을 확인할 수 있다.
그 이유는 sendMailIfRegisteredEmail의 테스트가 다른 세부 구현에 의존하고 있기 때문이다. 즉, sendMailIfRegisteredEmail는 isEmailRegistered에 대한 구현에 의존하고 있다. 이렇게 의존적인 테스트 코드를 작성하면 다음과 같은 문제가 있다.
- 테스트에 대한 관심이 여러가지며, 독립적이지 못하다.
- 변경이 전파될 수 있다.
위와 같은 테스트 코드는 여러가지 관심을 가져 단위 테스트로써 적합하지 않으며, 만약 isEmailRegistered라는 함수가 변하게 된다면 sendMailIfRegisteredEmail의 함수 역시 변경될 가능성이 있다.
물론 isEmailRegistered가 private 메소드라면 어찌할 방법이 없겠지만, public 메소드이기 때문에 개선의 여지가 있어 보인다.
[ 개선된 단위 테스트 - 독립적인 테스트 ]
우리가 해야 하는 것은 sendMailIfRegisteredEmail의 테스트가 1가지 관심만을 갖는 단위 테스트를 작성하는 것이다. 그러기 위해서는 다음과 같이 isEmailRegistered를 stub해줄 필요가 있어 보인다.
@ExtendWith(MockitoExtension.class)
class MailServiceTest {
@InjectMocks
private MailService mailService;
@Mock
private MailRepository mailRepository;
private final String email = "mangkyu@email.com";
@DisplayName("")
@Test
void sendMailIfRegisteredEmailSuccess() {
// given
doReturn(true).when(mailService).isEmailRegistered(email);
// when
final boolean result = mailService.sendMailIfRegisteredEmail(email);
// then
assertThat(result).isTrue();
// verify
verify(mailService, times(1)).sendMail(email);
}
@DisplayName("")
@Test
void sendMailIfRegisteredEmailFail_EmailNotRegistered() {
// given
doReturn(false).when(mailService).isEmailRegistered(email);
// when
final boolean result = mailService.sendMailIfRegisteredEmail(email);
// then
assertThat(result).isFalse();
// verify
verify(mailService, times(0)).sendMail(email);
}
}
하지만 위와 같이 테스트 코드를 작성하면 다음과 같은 에러가 발생한다.
Argument passed to when() is not a mock!
Example of correct stubbing:
doThrow(new RuntimeException()).when(mock).someMethod();
org.mockito.exceptions.misusing.NotAMockException:
Argument passed to when() is not a mock!
즉, 테스트 대상으로써 Mock 객체를 주입받는 MailService는 Mock 객체가 아니라 실제 객체이기 때문이 stub이 안된다는 것이다.
그래서 이에 대한 해결책으로 @Spy 어노테이션을 활용할 수 있다. 여기에서 @Spy는 Stub하지 않은 메소드들은 원본 메소드 그대로 사용하는 어노테이션이라고 설명하였다.
MailService에 @InjectMocks와 함께 @Spy를 붙여주면 우리는 isEmailRegistered를 stubbing하여 1개의 책임을 갖는 단위 테스트로 수정할 수 있다. 이렇게 하여 최종적으로 수정한 테스트 코드는 다음과 같다.
@ExtendWith(MockitoExtension.class)
class MailServiceTest {
@Spy
@InjectMocks
private MailService mailService;
@Mock
private MailRepository mailRepository;
private final String email = "mangkyu@email.com";
@DisplayName("")
@Test
void sendMailIfRegisteredEmailSuccess() {
// given
doReturn(true).when(mailService).isEmailRegistered(email);
// when
final boolean result = mailService.sendMailIfRegisteredEmail(email);
// then
assertThat(result).isTrue();
// verify
verify(mailService, times(1)).sendMail(email);
}
@DisplayName("")
@Test
void sendMailIfRegisteredEmailFail_EmailNotRegistered() {
// given
doReturn(false).when(mailService).isEmailRegistered(email);
// when
final boolean result = mailService.sendMailIfRegisteredEmail(email);
// then
assertThat(result).isFalse();
// verify
verify(mailService, times(0)).sendMail(email);
}
}
단위 테스트는 최대한 독립적으로 작성하여, 1가지 기능에 대해서만 테스트하는 것이 좋다.
만약 테스트 대상이 아닌 다른 코드에 의존성이 생긴다면, 테스트의 관심사가 여러 메소드에 걸쳐 존재하고 그에 따라 변경이 전파되는 등의 문제가 발생할 수 있다.