Mockito
단위 테스트를 위한 자바 Mocking 프레임워크 중 하나이다.
이 글을 작성한 시점에도 자바 진영에선 가장 보편적인 Mocking 프레임워크이며, 테스트 대역(Test Double)의 종류 중 모의(Mock) 객체를 필요로 할 때 사용한다.
다른 자바 Mocking 프레임워크에는 JMock, EasyMock 등이 있다.
💡 테스트 대역(Test Double)
테스트를 위해 실제 객체를 대체하는 것을 말한다.
💡 모의(Mock) 객체
호출했을 때 사전에 정의된 명세대로의 결과를 돌려주도록 미리 프로그램돼있는 테스트용 객체를 말한다.
테스트 대역, 모의 객체에 대해 더 자세히 알고 싶으신 분들은 테스트 대역(Test Double) 포스팅을 참고하기 바란다.
Stubbing
모의 객체 생성 및 모의 객체의 동작을 지정하는 것을 Stubbing이라고 한다.
예를 들면 아래와 같은 Stubbing이 가능한 것이다.
- 특정 매개변수를 받았을 때 특정 값을 반환하거나 예외를 던지도록 설정할 수 있다.
@Test
@DisplayName("아이디 중복으로 인한 유저 등록 실패")
public void FailToUserCreateIfDuplicateLoginId() throws Exception {
// findUserByLoginId 메서드가 호출될때 UserDTOAllField를 반환
when(userServiceImpl.findUserByLoginId("loginid123")).thenReturn(UserDTOAllField);
assertThrows(DuplicateKeyException.class,
() -> {
userServiceImpl.addUser(UserDTOAllField);
}
);
}
- thenReturn() 메서드나 thenThrow() 메서드를 이어 붙이는 구조를 사용하여
동일한 메서드가 여러 번 호출될 때 각각 다르게 행동하도록 할 수 있다.
@Test
public void stubbingChaining() {
// thenReturn() 메서드를 이어 붙여서 29와 31을 반환하게 했다.
when(mock.length()).thenReturn(29).thenReturn(31);
assertEquals(29, mock.length());
assertEquals(31, mock.length());
}
- Mock 객체 메서드의 파라미터 값을 하드 코딩하고 싶지 않다면
Mockito의 Argument Matchers를 이용하면 된다.
@Test
public void stubbingGenericArgument() {
// anyInt() 메서드로 mock 객체의 get() 메서드 파라미터에 타입만 일치한다면
// 어떠한 값이 와도 beststar를 반환하도록 설정했다.
when(mock.get(anyInt())).thenReturn("beststar");
assertEquals("beststar", mock.get(29));
assertEquals("beststar", mock.get(31));
}
💡 이 밖에도 여러 가지 Stubbing이 있으며 자세한 내용은 Mockito 공식문서를 번역한 Mockito features in Korean를 참고 바란다.
Verification
테스트하고자 하는 메서드가 의도한 대로 동작하는지 검증하는 것을 말한다.
Mockito.verify(mock).action() 의 구조로 사용이 가능하며 호출 여부, 횟수, 순서, 타임아웃을 검증할 수 있다.
// mock 생성
List firstMock = mock(List.class);
List secondMock = mock(List.class);
// mock 객체 사용
firstMock.add(“one”);
// add("one")이 한번 호출됐는지 검증
verify(firstMock).add(“one”);
// clear()가 호출됐는지 검증
verify(firstMock).clear();
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// mock이 순서대로 실행되는지 확인하기 위해 inOrder 객체에 mock을 전달
InOrder inOrder = inOrder(firstMock, secondMock);
// firstMock이 secondMock 보다 먼저 실행되는 것을 확인
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
//주어진 시간 안에 someMethod()가 적어도 2번 호출되면 성공
verify(mock, timeout(100).atLeast(2)).someMethod();
// 주어진 시간 안에 someMethod()가 지정된 검증 방식을 통과하면 성공
// 자신만의 검증 방식이 만들어져 있다면 유용한 방식
verify(mock, new Timeout(100, yourOwnVerificationMode)).someMethod();
Mock 객체를 만드는 방법
Mockito.mock(대상 클래스)
class BestStarServiceTest {
@Test
void test() {
BestStarService bestStarService = Mockito.mock(BestStarService.class);
BestStarRepository bestStarRepository = Mockito.mock(BestStarRepository.class);
GoodService goodService = new GoodService(bestStarService, bestStarRepository);
}
}
위 코드처럼 Mock 객체의 대상이 되는 클래스를 mock() 메서드의 파라미터로 넣어서 테스트 메서드 내부에서 사용할 수 있다.
@Mock
@ExtendWith(MockitoExtension.class)
public class UserCreateTest {
// @Mock 어노테이션을 적용한 UserMapper Mock 객체 생성
@Mock
private UserMapper userMapper;
// Mock 객체를 주입(Inject)받을 UserServiceImpl 객체에 @InjectMocks 어노테이션 적용
@InjectMocks
private UserServiceImpl userServiceImpl;
private UserDTO UserDTOAllField;
@BeforeEach
void init() {
UserDTOAllField = UserDTO.builder().
loginId("loginid123").
name("황사이다").
birthDate(LocalDate.of(2000,11,11)).
sex(UserDTO.Sex.MALE).
password("비1밀2번3호").
nickname("닉네임123이다").
phoneNumber("01012345678").
build();
}
@Test
@DisplayName("모든 UserDTO 데이터가 입력된 경우 유저 등록 성공")
void SuccessUserCreateIfAllFieldInserted() {
// userServiceImpl의 addUser 메서드가 Exception을 throw 하지 않는지 확인
assertDoesNotThrow(() -> userServiceImpl.addUser(UserDTOAllField));
}
}
Mockito.mock() 메서드를 사용하면 자주 사용되는 Mock 객체일수록 코드가 반복될 텐데,
위 코드의 userMapper 필드처럼 Mock 객체의 대상이 되는 클래스에 @Mock 어노테이션을 적용할 수 있고 반복되는 코드도 줄일 수 있다.
💡 위 예제 코드는 토이 프로젝트에서 가져온 것이므로 다른 테스트 코드가 궁금하신 분들은 참고 바란다.
@ExtendWith(MockitoExtension.class)
class BestStarServiceTest {
@Test
void test(@Mock BestStarService bestStarService) {
GoodService goodService = new GoodService(bestStarService);
}
}
참고로 위 코드처럼 파라미터로 넘겨줘서 특정 테스트 메서드에서만 사용 가능하도록 만들 수도 있다.
@Mock 동작 원리
아래는 바로 위의 @Mock 어노테이션 적용 코드에 대해 디버깅을 하고 분석해본 스크린숏이다.
우선 Mock 객체의 이름 외에도 직렬화 할 것인지(serializable),
메서드 호출을 트래킹 하지 않아서 메모리를 절약할 것인지(stubOnly),
테스트 간 파라미터가 잘못 입력됐거나 불필요한 Mock 객체를 사용하지 않고 방치한 것에 대해서도 별다른 경고 없이 넘어가고 싶은지(lenient) 등
부가적인 Mock 관련 세팅값들이 어노테이션에서 넘어오면 하나씩 세팅한 다음
💡 참고로 Stubbing 방식을 기준으로 봤을 때 lenient(관대)하다면 Loose Stubbbing, 그렇지 않다면 Strict Stubbing으로 부를 수도 있다.
@Mock 어노테이션이 적용된 객체의 타입을 검사하여 타입에 따라 Mock 객체를 생성하고 반환한다.
위 예제 코드의 경우에는 UserMapper 타입이라서 분기를 타고 Mockito.mock() 메서드가 호출되고 반환된다.
이렇게 내부적으로는 MockAnnotationProcessor라는 클래스 덕분에 Mockito.mock()가 반복되는 코드를 방지해 주는 것이다.
@InjectMocks
해당 테스트 클래스 내에서 이 어노테이션이 적용된 객체에 Mock 객체를 자동으로 주입해주는 것이다.
@ExtendWith(MockitoExtension.class)
public class UserCreateTest {
// @Mock 어노테이션을 적용한 UserMapper Mock 객체 생성
@Mock
private UserMapper userMapper;
// Mock 객체를 주입(Inject)받을 UserServiceImpl 객체에 @InjectMocks 어노테이션 적용
@InjectMocks
private UserServiceImpl userServiceImpl;
}
위 코드에서는 UserMapper 타입의 Mock 객체를 UserServiceImpl 객체에 주입하는 예시이다.
@ExtendWith(MockitoExtension.class)
위에서 설명했던 Mock 객체들도 결국 해당 테스트 클래스에 @ExtendWith(MockitoExtension.class)를 적용하지 않으면 생성되지 않는다.
💡 @ExtendWith 어노테이션은 JUnit에서 해당 테스트 클래스에 공통으로 적용할 기능이 있을 때 사용하는 것이다.
만약 @ExtendWith(MockitoExtension.class)를 적용하지 않는다면 각 테스트 메서드 실행 전에 MockitoAnnotations.initMocks(this)를 추가해줘야 한다.
💡 테스트 메서드 실행 전에 추가한다는 말이 이해가 가지 않는다면 JUnit 대표적 단정(Assert) 메서드, 라이프사이클(Lifecycle) 메서드 포스팅을 참고 바란다.
@ExtendWith(MockitoExtension.class)
public class UserCreateTest {
@Mock
private UserMapper userMapper;
@InjectMocks
private UserServiceImpl userServiceImpl;
...
}
@ExtendWith 어노테이션의 값으로 MockitoExtension 클래스를 사용했는데,
이 클래스에는 Mockito 문법을 잘못 사용했는지 검사하는 기능이 포함돼있다.
@Test
@DisplayName("아이디 중복으로 인한 유저 등록 실패")
public void FailToUserCreateIfDuplicateLoginId() throws Exception {
given(userServiceImpl.findUserByLoginId("loginid123")); // willReturn() 메서드는 어디에??
assertThrows(DuplicateKeyException.class,
() -> {
userServiceImpl.addUser(UserDTOAllField);
}
);
}
위 테스트 메서드의 given() 우측에는 올바른 문법이라면 findUserByLoginId() 메서드가 호출되면 어떤 것을 반환할지 정의돼야 하는데 willReturn() 메서드가 없어서 잘못된 문법이다. 이러면 UnfinishedStubbingException 예외가 발생하며 에러 메시지에는 올바른 사용 예시까지 안내돼있다.
출처
@ExtendWith(MockitoExtension.class)과 @Mock 사용법
예제 코드 : 토이 프로젝트 - 뉴스
MockSettings 중 lenient
javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/MockSettings.html#lenient--
ikeptwalking.com/strict-mocking-vs-loose-mocking/
Mockito Features 한글 번역 문서
'테스트 코드 > Mockito' 카테고리의 다른 글
BDDMockito (0) | 2021.04.27 |
---|