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 사용법

javadoc.io/doc/org.mockito/mockito-junit-jupiter/latest/org/mockito/junit/jupiter/MockitoExtension.html

 

예제 코드 : 토이 프로젝트 - 뉴스

github.com/HwangWonGyu/news

 

HwangWonGyu/news

Contribute to HwangWonGyu/news development by creating an account on GitHub.

github.com

 

MockSettings 중 lenient

javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/MockSettings.html#lenient--

ikeptwalking.com/strict-mocking-vs-loose-mocking/

 

Mockito Features 한글 번역 문서

github.com/mockito/mockito/wiki/Mockito-features-in-Korean

'테스트 코드 > Mockito' 카테고리의 다른 글

BDDMockito  (0) 2021.04.27