Mockito란 가짜 객체를 만들어 실제 객체의 행동을 지정해 테스트 할 수 있는 방법을 제공해주는 라이브러리이다.
Mockito또한 Junit5와 같은 테스트 기반 프레임워크이며,
기존에 @RunWith(MockitoJUnitRunner.class)로 Junit과 Mockito를 연동했다면, SpringBoot 2.2.0 부터는 공식적으로 JUnit5를 지원함에 따라, @ExpendWith(MockitoExtension.class)를 사용해야 결합이 가능하다.
실제 계좌 생성 코드를 통해 Mockito를 활용한 단위테스트를 작성해보자.
[account entity]
@Getter
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false, length = 20)
private Long number; // 계좌번호
@Column(nullable = false, length = 4)
private Long password; // 계좌비밀번호
@Column(nullable = false)
private Long balance; // 잔액 (디폴트 값 1000원)
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
@CreatedDate
@Column(nullable = false)
private LocalDateTime createdAt;
}
[account service 코드]
아래는 사용자의 계좌등록을 하는 비즈니스 로직이다.
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class AccountService {
private final Logger log = LoggerFactory.getLogger(getClass());
private final UserRepository userRepository;
private final AccountRepository accountRepository;
private final TransactionRepository transactionRepository;
@Transactional
public AccountSaveRespDto 계좌등록(AccountSaveReqDto accountSaveReqDto, Long userId) {
User userPS = userRepository.findById(userId).orElseThrow(
() -> new CustomApiException("유저를 찾을 수 없습니다"));
Optional<Account> isUseAccountOP = accountRepository.findByNumber(accountSaveReqDto.getNumber());
if (isUseAccountOP.isPresent()) {
throw new CustomApiException("해당 계좌가 이미 존재합니다");
}
Account accountPS = accountRepository.save(accountSaveReqDto.toEntity(userPS));
return new AccountSaveRespDto(accountPS);
}
[account service 테스트]
@ExtendWith(MockitoExtension.class)
public class AccountServiceTest extends DummyObject {
private final Logger log = LoggerFactory.getLogger(getClass());
@InjectMocks
private AccountService accountService;
@Mock
private AccountRepository accountRepository;
@Mock
private UserRepository userRepository;
@Mock
private TransactionRepository transactionRepository;
@Spy
private ObjectMapper om;
@Test
public void 계좌등록_test() throws Exception {
// given
AccountSaveReqDto accountSaveReqDto = new AccountSaveReqDto();
accountSaveReqDto.setNumber(1111L);
accountSaveReqDto.setPassword(1234L);
// stub 1
User ssar = newMockUser(1L, "ssar", "쌀");
when(userRepository.findById(ssar.getId())).thenReturn(Optional.of(ssar));
// stub 2
// Account isUseAccount = newMockAccount(1L, 1111L, 1000L, ssar);
when(accountRepository.findByNumber(1111L)).thenReturn(Optional.empty());
// stub 3
Account ssarAccount = newMockAccount(1L, 1111L, 1000L, ssar);
when(accountRepository.save(any())).thenReturn(ssarAccount);
// when
AccountSaveRespDto accountSaveRespDto = accountService.계좌등록(accountSaveReqDto, ssar.getId());
String responseBody = om.writeValueAsString(accountSaveRespDto);
log.debug("테스트 : " + responseBody);
// then
assertThat(accountSaveRespDto.getNumber()).isEqualTo(1111L);
}
위의 3가지 코드들은 모두 repository가 수행하는 부분이다.
즉, service 코드를 test할때 repository의 코드는 테스트할 필요가 없다.
우리는 service 코드를 테스트 하는 것이기에 repository의 코드는 테스트하지 않는다.
기존의 springbootTest에서는 repository 역시 스프링컨테이너에 빈으로 등록하여, 해당 메서드들을 실행하게 되는데, 이러면 테스트에 불필요한 의존성이 추가되며, spring 기반으로 테스트가 진행되기에 spring 컨테이너를 띄우는데 오랜 시간이 걸린다.
=> 단위 테스트의 어려움.
그렇기에 테스트하지 않는 부분의 의존성은 mock을 사용하여 가짜 객체로 만든 후 service 메서드에서 가짜 객체의 행동을 정의하여 테스트를 수행하면 된다.
stubbing하는 코드를 좀 더 자세히 살펴보자.
stub3 같은 경우에 any()를 사용했는데, save메서드의 매개변수에 어떤 형태의 인자든 모두 올 수 있다는 의미이다.
anyLong()등과 같은 argumentmatcher를 사용하여 매개변수 타입을 지정해 줄 수 있다.