오늘은 가장 대중적인 자바 단위 테스트 프레임워크인 Mockito 에 대해 알아보겠습니다 🧑🏼💻
Mockito는 개발자가 동작을 직접 제어할 수 있는 가짜(Mock) 객체를 지원하는 테스트 프레임 워크 입니다.
모의 객체(Mock Object)란 주로 객체 지향 프로그래밍으로 개발한 프로그램을 테스트할 경우 테스트를 수행할 모듈과 연결되는 외부의 다른 서비스나 모듈들을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는 데 사용하는 객체이다.
일반적으로 Spring으로 웹 어플리케이션을 개발하면, 여러 객체들 간의 의존성이 생기기 마련입니다.
물론 Spring Container 가 이를 관리하지만 단위 테스트를 작성시 문제가 발생합니다 🤔
모듈간 의존성으로 인한 단위 테스트 작성의 어려움에 대한 예시를 하나 보겠습니다 🧐
@Service
@RequiredArgsConstructor
public class AccountService { // [계좌] 관련 Service
private final UserRepository userRepository;
private final AccountRepository accountRepository;
public Account createAccount(Long userId, Long balance) { // 계좌 생성을 위한 로직
// 1. userRepository 에서 해당 userId 회원 있는지 조회
// 2. accountRepository 에서 가장 최근에 개설한 계좌 번호 조회
// 3. 가장 최근에 개설한 계좌 번호 + 1(새로 개설한 계좌번호) accountRepostory 저장
}
}
해당 코드는 계좌 관련 Service 로직을 구현한 코드입니다.
createAccount 메소드를 통해 userId(회원 번호) 와 balance(초기 잔액) 을 입력받고, 계좌를 생성하는 로직입니다.
AccountService 클래스는 두 유형의 객체에 의존하고 있습니다.
바로 UserRepository와 AccountRepository 인데요 💡
하나의 계좌를 생성할 때, DB 접근 횟수가 3번(조회2 + 저장1)이나 됩니다.
이를 테스트 코드로 만들게 되면 매번 테스트 할때마다 DB에 접근하는 것이 부하가 많이 걸리고 시간이 오래 걸릴 수 있습니다.
그렇기 때문에 사용자가 의도적으로 '가짜 객체' 즉, mock으로 만들어서 DB 접근을 최소화 할 수 있습니다.
그리고 Mockito 프레임워크는 mock을 쉽게 만들고 mock 행동을 정하는 stubbing, 정상적으로 동작하는지에 대한 verify 등 다양한 기능을 제공해줍니다 💪
mock 생성과 관련된 어노테이션은 @Mock, @Spy, @InjectMock 이 있습니다.
@ Mock 으로 만든 mock 객체는 가짜 객체이며 그 안에 메소드 호출해서 사용하려면 반드시 스터빙(stubbing)을 해야 합니다.
스터빙을 거치지 않으면 null 을 반환하여 원하는 테스트를 할 수 없습니다.
@ InjectMocks 는 DI를 @Mock이나 @Spy로 생성된 mock 객체를 자동으로 주입해주는 어노테이션입니다.
@ Spy로 만든 mock 객체는 진짜 객체이며 메소드 실행 시 스터빙을 하지 않으면 기존 객체의 로직을 실행한 값을, 스터빙을 한 경우엔 스터빙 값을 리턴합니다.
public class ProductService {
public Product getProduct() {
return new Product("A001", "monitor");
}
}
@ExtendWith(MockitoExtension.class)
public class InjectMocksAnnotation {
@Mock
UserService userService; // 가짜 객체
@Spy
ProductService productService; // 진짜 객체
@InjectMocks
OrderService orderService; // orderService DI 주입
@Test
void testGetUser() {
assertNull(orderService.getUser()); // stubbing x
}
@Test
void testGetProduct() {
Product product = orderService.getProduct();
assertEquals("A001", product.getSerial());
}
}
개인적인 경험으로 스터빙은 Mockito 프레임워크를 학습시, 가장 중요한 개념이라고 생각합니다 🧑🏼💻
스터빙(stubbing) 이란 mock 객체의 메소드를 실행 시 어떤 리턴 값을 리턴 할지를 미리 정의하는 것
결국 위에 정의처럼 스터빙이란 개발자가 자신이 작성한 코드를 검사하기 위해 연관된 mock 객체의 메소드의 결과를 조작하는 것을 의미합니다 💡
Mockito 에서는 when 메소드를 이용해서 스터빙을 지원합니다.
when에 스터빙할 메소드를 넣고 그 이후에 어떤 동작을 어떻게 제어할지를 메소드 체이닝 형태로 작성하면 됩니다.
메소드 명 | 설명 |
---|---|
when | 스터빙 조건 |
thenReturn | 스터빙한 메소드 호출 후 어떤 객체를 리턴할 건지 정의 |
thenThrow | 스터빙한 메소드 호출 후 어떤 Exception 을 Throw 할지 정의 |
현재 진행중인 계좌 시스템(Account System)의 AccountServiceTest(계좌 관련 서비스 테스트 클래스) 코드를 통해 스터빙을 이해해보겠습니다 🧑🏼💻
@ExtendWith(MockitoExtension.class)
class AccountServiceTest {
@Mock
private AccountRespository accountRespository;
@Mock
private AccountUserRepository accountUserRepository;
@InjectMocks
private AccountService accountService;
@Test
@DisplayName("계좌 생성 성공")
void createAccountSuccess() {
AccountUser accountUser = AccountUser.builder()
.id(12L)
.name("pobi").build();
when(accountUserRepository.findById(anyLong()))
.thenReturn(Optional.of(accountUser));
when(accountRespository.findFirstByOrderByIdDesc())
.thenReturn(Optional.of(Account.builder()
.accountNumber("1000000013").build()));
ArgumentCaptor<Account> captor = ArgumentCaptor.forClass(Account.class);
when(accountRespository.save(any()))
.thenReturn(Account.builder()
.accountUser(accountUser)
.accountNumber("1000000013").build());
// given
// when
AccountDto dto = accountService.createAccount(1L, 1000L);
// then
assertEquals(12L, dto.getUserId());
assertEquals("1000000013", dto.getAccountNumber());
}
AccountServiceTest 에서 계좌 생성성공 을 테스트하는 메소드인 createAccountSuccess 부분을 보겠습니다 🧑🏼💻
✅ 테스트 하기 위해 필요한 것들
when으로 시작하는 부분은 스터빙 시작 부분입니다.
1. accountUserRepostory.findById() 수행시 id가 12 name이 'pobi' 인 accountUser가 반환될 것입니다.
2. accountRepostory.findFirstByOrderByDesc() 수행시 accountNumber 가 1000000013 인 account가 반환될 것입니다.
3. 해당 스터빙을 통해 계좌를 저장하면 1,2 번에서 정의한 accountUser와 accountNumber 을 멤버로 가지는 Account 객체가 리턴될 것입니다.
위에 가정한 1 ~ 3 은 스터빙 과정입니다.
이를 통해 실제 accountService.createAccount 실행시, 어떤 경우에라도 스터빙 한 결과가 나와야 합니다.
그렇지 않으면 테스트 실패입니다 😕
현재 TDD 에서는 대다수가 Mockito + JUnit 조합을 사용합니다.
마지막은 JUnit 을 이용해 검증합니다 🔑
assertEquals(12L, dto.getUserId());
assertEquals("1000000013", dto.getAccountNumber()); // 스터빙 결과와 동일하게나와야 합니다.
검증(Verify) 는 스터빙한 메소드가 제대로 실행이 되는지 확인하는 기능입니다.
verify 관련 메소드를 이용해서 여러가지를 검증할 수 있습니다.
메소드 명 | 설명 |
---|---|
times(n) | 몇 번이 호출됐는지 |
never | 한 번도 호출되지 않았는지 |
atLeastOne | 최소 한 번은 호출됐는지 |
atLeast(n) | 최소 n번이 호출됐는지 |
atMostOnce | 최대 한 번은 호출됐는지 |
atMost(n) | 최대 n번이 호출됐는지 |
calls(n) | n번이 호출됐는지 |
timeout(long mills) | n ms 이상 걸리면 fail |
after(long mills) | timeout과 달리 n ms 지나도 검증 종료되지 않음 |
description | 실패한 경우 나올 문구 |
@Test
public void example(){
Person p = mock(Person.class);
String name = "JDM";
p.setName(name);
// n번 호출했는지 체크
verify(p, times(1)).setName(any(String.class)); // success
// 호출 안했는지 체크
verify(p, never()).getName(); // success
verify(p, never()).setName(eq("ETC")); // success
verify(p, never()).setName(eq("JDM")); // fail
// 최소한 1번 이상 호출했는지 체크
verify(p, atLeastOnce()).setName(any(String.class)); // success
// 2번 이하 호출 했는지 체크
verify(p, atMost(2)).setName(any(String.class)); // success
// 2번 이상 호출 했는지 체크
verify(p, atLeast(2)).setName(any(String.class)); // fail
// 지정된 시간(millis)안으로 메소드를 호출 했는지 체크
verify(p, timeout(100)).setName(any(String.class)); // success
// 지정된 시간(millis)안으로 1번 이상 메소드를 호출 했는지 체크
verify(p, timeout(100).atLeast(1)).setName(any(String.class)); // success
}
이번 포스팅에서는 기본적인 Mockito 사용법에 대해 알아보았습니다 👨💻
자바 진영의 대표적인 단위 테스트 프레임워크인 Mockito는 자바 개발자라면 꼭 알아야하는 테스팅 방법입니다 💡
또한 JUnit5 와 함께 자주 사용됨으로 같이 숙지해야할 필요가 있습니다.
추후 좀 더 구체적으로 상황에 따른 Mockito 사용법에 대해 포스팅하겠습니다.
Mockito @Mock @MockBean @Spy @SpyBean 차이점
Mockito란? Mockito 사용하기