본문 바로가기

Study/개발일지

[백엔드TIL] Mockito를 활용한 단위테스트 (76일차)

[ Mockito란? ]

Mockito는 개발자가 동작을 직접 제어할 수 있는 가짜 객체를 지원하는 테스트 프레임워크이다. 일반적으로 Spring으로 웹 애플리케이션을 개발하면, 여러 객체들 간의 의존성이 생긴다. 이러한 의존성은 단위 테스트를 작성을 어렵게 하는데, 이를 해결하기 위해 가짜 객체를 주입시켜주는 Mockito 라이브러리를 활용할 수 있다. Mockito를 활용하면 가짜 객체에 원하는 결과를 Stub하여 단위 테스트를 진행할 수 있다. 물론 프레임워크 도구가 필요없다면 사용하지 않는 것이 가장 좋다.

 

 

 

[ Mockito 사용법 ]

1. Mock 객체 의존성 주입

Mockito에서 가짜 객체의 의존성 주입을 위해서는 크게 3가지 어노테이션이 사용된다.

  • @Mock: 가짜 객체를 만들어 반환해주는 어노테이션
  • @Spy: Stub하지 않은 메소드들은 원본 메소드 그대로 사용하는 어노테이션
  • @InjectMocks: @Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 어노테이션

 

예를 들어 UserController에 대한 단위 테스트를 작성하고자 할 때, UserService를 사용하고 있다면 @Mock 어노테이션을 통해 가짜 UserService를 만들고, @InjectMocks를 통해 UserController에 이를 주입시킬 수 있다.

 

 

 

2. Stub로 결과 처리

앞서 설명하였듯, 의존성이 있는 객체는 가짜 객체를 주입하여 어떤 결과를 반환하라고 정해진 답변을 준비시켜야 한다. Mockito에서는 다음과 같은 stub 메소드를 제공한다.

  • doReturn(): 가짜 객체가 특정한 값을 반환해야 하는 경우
  • doNothing(): 가짜 객체가 아무 것도 반환하지 않는 경우(void)
  • doThrow(): 가짜 객체가 예외를 발생시키는 경우

 

 

 

3. Mockito와 Junit의 결합

Mockito도 테스팅 프레임워크이기 때문에 JUnit과 결합되기 위해서는 별도의 작업이 필요하다. 기존의 JUnit4에서 Mockito를 활용하기 위해서는 클래스 어노테이션으로 @RunWith(MockitoJUnitRunner.class)를 붙여주어야 연동이 가능했다. 하지만 SpringBoot 2.2.0부터 공식적으로 JUnit5를 지원함에 따라, 이제부터는 @ExtendWith(MockitoExtension.class)를 사용해야 결합이 가능하다.

 

 

Spring 서비스 계층 단위 테스트 작성 예시


[ 사용자 회원가입/목록 조회 비지니스 로직 ]

사용자 회원가입과 목록 조회를 위해서는 다음과 같은 비지니스 로직 레이어(Service Layer)가 필요하다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder;

    @Transactional
    public UserResponse signUp(final SignUpRequest request) {
        final User user = User.builder()
                .email(request.getEmail())
                .pw(passwordEncoder.encode(request.getPw()))
                .role(UserRole.ROLE_USER)
                .build();

        return UserResponse.of(userRepository.save(user));
    }

    public List<User> findAll() {
        return userRepository.findAll().stream()
            .map(UserResponse::of)
            .collect(Collectors.toList()));
    }
}

 

 

단위 테스트(Unit Test) 작성 준비

앞서 설명하였듯 @ExtendWith(MockitoExtension.class)와 가짜 객체 주입을 사용해 다음과 같은 테스트 클래스를 작성할 수 있다.

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @InjectMocks
    private UserService userService;

    @Mock
    private UserRepository userRepository;

    @Spy
    private BCryptPasswordEncoder passwordEncoder;

}

 

 

이번에는 BCryptPasswordEncoder에 @Spy를 사용하였다. 앞서 설명하였듯 Spy는 Stub하지 않은 메소드는 실제 메소드로 동작하게 하는데, 위의 예제에서 실제로 사용자 비밀번호를 암호화해야 하므로, @Spy를 사용하였다. 이번에도 테스트 코드를 작성해보도록 하자.

  1. 회원가입 성공
  2. 사용자 목록 조회

 

 

1. 회원가입 성공 테스트

@DisplayName("회원 가입")
@Test
void signUp() {
    // given
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    SignUpRequest request = signUpRequest();
    String encryptedPw = encoder.encode(request.getPw());

    doReturn(new User(request.getEmail(), encryptedPw, UserRole.ROLE_USER)).when(userRepository)
        .save(any(User.class));
        
    // when
    UserResponse user = userService.signUp(request);

    // then
    assertThat(user.getEmail()).isEqualTo(request.getEmail());
    assertThat(encoder.matches(signUpDTO.getPw(), user.getPw())).isTrue();

    // verify
    verify(userRepository, times(1)).save(any(User.class));
    verify(passwordEncoder, times(1)).encode(any(String.class));
}

 

 

이번에는 추가적으로 mockito의 verify()를 사용해보았다. verify는 Mockito 라이브러리를 통해 만들어진가짜 객체의 특정 메소드가 호출된 횟수를 검증할 수 있다. 위에서는 passwordEncoder의 encode 메소드와 userRepository의 save 메소드가 각각 1번만 호출되었는지를 검증하기 위해 사용하였다.

 

 

 

2. 사용자 목록 조회 테스트

@DisplayName("사용자 목록 조회")
@Test
void findAll() {
    // given
    doReturn(userList()).when(userRepository)
        .findAll();

    // when
    final List<UserResponse> userList = userService.findAll();

    // then
    assertThat(userList.size()).isEqualTo(5);
}

private List<User> userList() {
    List<User> userList = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
        userList.add(new User("test@test.test", "test", UserRole.ROLE_USER));
    }
    return userList;
728x90