단위테스트 ‐ Service 계층 - f-lab-edu/jshop GitHub Wiki

서비스 계층은 컨트롤러에서 받은 데이터에 대해 비즈니스로직을 수행하고 Repository 계층으로 넘기는 핵심 계층이다.

서비스 계층의 경우 Repository 계층을 통해 데이터를 다루기 때문에 Repository 계층에 의존하게 된다.

하지만 단위테스트에서 서비스 계층에 대한 비즈니스 로직을 검증해야 하는데 다른 계층까지 의존하게 된다면 서비스 계층 검증에 대한 신뢰성이 낮아질 수 있다.

테스트의 신뢰성을 높이기 위해서는 서비스 계층만 독립적인 테스트를 진행해야 하고 의존관계에 있는 요소들은 테스트 더블을 사용해 테스트를 수행할 예정이다.

Service 계층에서 검증할 내용은, Controller 계층에서 넘어온 데이터가 비즈니스로직을 거쳐 Repository 계층까지 잘 넘어가는가 이다.

Mockito

테스트 더블을 사용할수록 지원하는 프레임워크다. 고립된 단위테스트에서 의존관계에 있는 모듈의 동작을 모방하기 위해 사용한다.

예를들어 Service 레이어에서 Repository 레이어를 사용하고, Service 레이어를 테스트하기 위해서는 Repository 레이어를 Mock으로 만들어 사용할 수 있다.

다음은 UserService의 테스트 코드다.

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

  @Mock
  private UserRepository userRepository;

  @Spy
  private BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();

  @InjectMocks
  private UserService userService;

  ...
}
  • ExtendsWith(MockitoExtension.class) : junit5 에서 Mockito를 사용하기 위한 확장이다.
  • @Mock : 목 객체를 생성한다. 이 객체는 실제 객체의 동작을 모방하기 위해 사용한다. 위의 userRepository의 save 메서드를 사용하면, 영속성컨텍스트와 DB에 저장하지 않고 단순 정의된 동작에 따라 동작한다
  • @Spy : 실제 객체의 메서드를 유지한 객체를 생성한다. 기존 동작을 사용할때 유용하다.
  • @InjectMocks : 테스트를 위해 목을 주입받을 객체다. 한마디로 테스트할 객체다.

테스트 코드 예시

회원가입 테스트

위에서 언급했듯 Service 계층 테스트에서 중요한 요소는 입력 데이터를 적절하게 다뤄 다음 계층으로 넘겨주는가이다.

이를 확인하기 위해 레포지토리 계층을 @Mock 으로 만들고 호출을 검증하기로 했다.

verify(userRepository, times(1)).save(userCaptor.capture());

userRepositorysave() 메서드가 몇번 호출되고, 호출될때의 인수를 캡쳐해 어떤값이 넘어갔는지를 확인하게 된다.

전체 코드를 보면 다음과 같다.

    JoinDto joinDto = getJoinDto(username, email, password, userType);
    User user = getJoinUser(username, email, password, userType, role);

    // when
    userService.joinProcess(joinDto);

    // then
    verify(userRepository, times(1)).save(userCaptor.capture());
    User capturedUser = userCaptor.getValue();

    assertThat(capturedUser.getUsername()).isEqualTo(user.getUsername());
    assertThat(bCryptPasswordEncoder.matches(password, capturedUser.getPassword())).isTrue();
    assertThat(capturedUser.getEmail()).isEqualTo(user.getEmail());
    assertThat(capturedUser.getUserType()).isEqualTo(user.getUserType());

회원가입시에는 userRepository.save() 가 한번 호출되어야 하고, 이때 전달될 인수에 대한 검증이 이루어진다.

중복 회원가입 테스트

또다른 예시는 중복회원가입 예시다.

userRepository.existsByEmail() 메서드는 이미 가입된 회원을 조회하면 true 를 리턴하게 된다.

하지만 서비스 계층의 단위테스트에서는 데이터베이스를 사용하지 않기 때문에 이런 동작을 모킹해야 한다.

@Mock 으로 생성한 객체는 그 자체로는 아무런 동작도 하지 않는다. 때문에 해당 메서드가 실행되면 어떠한 동작을 할지 개발자가 정의해줘야 한다.

즉 어떠한 이메일로 이 메서드가 호출되면 true 를 리턴하라고 정의해두고 또다시 같은 회원이 가입하게 된다면 true 를 리턴해 비즈니스 로직에서 예외를 던지는지에 대해 검증하는 코드인것이다.

  @Test
  public void 중복회원가입() {
    // given
    String username = "test";
    String email = "[email protected]";
    String password = "test";
    UserType userType = UserType.USER;
    String role = "ROLE_USER";

    JoinDto joinDto = getJoinDto(username, email, password, userType);

    // when
    userService.joinProcess(joinDto);
    when(userRepository.existsByEmail(email)).thenReturn(true);

    // then
    assertThrows(AlreadyRegisteredEmailException.class, () -> userService.joinProcess(joinDto));
  }

단위 테스트는 가장 작은 단위로 쪼개 고립된 테스트를 진행하는것이 중요하다. 떄문에 목과 같은 도구들을 사용해, 의존관계의 객체를 사용하지 않고 테스트를 진행한다.