본문 바로가기
Spring Framework/TDD

Mockito

by 도쿠니 2022. 6. 21.

Mockito란?

  • 자바에서 단위테스트를 하기 위한 Mock Object를 creation(생성), verification(검증), stubbing 해주는 프레임워크
  • Spring Boot에는 자동으로 포함되어 있습니다.

Mock Object

  • 테스트를 수행할 객체가 의존하는 다른 객체들을 흉내내는 가짜 객체
  • 가짜 객체를 만들어 사용함으로서 외부에 의존하지 않는 순수한 단위테스트가 가능해집니다.

Mock Object Creation 🔥

Inline 방식Annotation 방식이 있습니다.

mock()

  • 파라미터로 들어온 객체의 Mock을 생성합니다.
  • 생성된 Mock Object는 스터빙을 해서 사용해야합니다.
MemberService memberService = Mockito.mock(MemberService.class);
StudyRepository studyRepository = mock(StudyRepository.class);

@Mock

  • mock()과 동일합니다.

사용방법

  1. 테스트 클래스에 @ExtendWith(MockitoExtension.class) 선언
  2. Mock Object로 사용할 곳에 @Mock 선언
    1. 필드 및 테스트 메소드 매개변수에 선언 가능합니다.
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
		@Mock 
		MemberService memberService;

		...
}

@Spy

  • @Mock 과 달리 진짜 객체를 생성합니다.
  • 메소드 실행 시 스터빙을 하지 않으면 기존 객체의 로직을 실행한 값을, 스터빙을 한 경우엔 스터빙 값을 리턴합니다.
  • spy() 로 대체하여 사용해도 됩니다.

@InjectMocks

  • 해당 어노테이션을 선언한 객체의 DI를 @Mock 이나 @Spy로 생성된 Mock Object로 주입합니다.

Mock Object Stubbing 🔥

Mock 객체는 가짜 객체이기 때문에 실제로 메소드 호출을 하면 제대로 작동하지 않습니다.

그렇기 때문에 메소드에 대한 Stubbing을 해주어야 합니다.

Stubbing을 하지 않고 그냥 호출하면 primitive type은 기본 값을, 참조 타입은 null을 반환합니다.

Stubbing

  • Test Stub을 의미하며 테스트 중에 만들어진 호출에 대해 미리 준비된 답변을 을 제공하는 것을 의미합니다.
  • 다른 객체를 흉내낸 Mock Object의 메소드를 실행했을 때 어떤 값을 리턴할지 정의하는 것이라고 생각하면 됩니다.
💡 Mockito에선 when 메소드를 이용해서 스터빙을 지원하고 있습니다.
     when에 스터빙할 메소드를 넣고 그 이후에 어떤 동작을 어떻게 제어할지를 메소드 체이닝 형태로 작성합니다.
     스터빙 방법으로는 OngoingStubbing 과 Stubber를 쓰는 방법이 있습니다.

Ongoing Stubbing Method

// when({Mock Object의 스터빙할 메소드}).{Ongoing Stubbing 메소드}
// ongoing 메소드를 체인해서 계속 사용하면, 호출마다 반환값이 바뀝니다.

@ExtendWith(MockitoExtension.class)
public class OngoingStubbingMethod {
 
    @Mock
    ProductService productService;
 
    @Test
    void testThenReturn() {
        Product product = new Product("T001", "mouse");
 
        when(productService.getProduct()).thenReturn(product);
 
        assertThat(productService.getProduct()).isEqualTo(product);
    }
 
    @Test
    void testThenThrows() {
        when(productService.getProduct()).thenThrow(new IllegalArgumentException());
 
        assertThatThrownBy(() -> productService.getProduct()).isInstanceOf(IllegalArgumentException.class);
    }
 
    @Test
    void testThenCallRealMethod() {
        when(productService.getProduct()).thenCallRealMethod();
 
        assertThat(productService.getProduct().getSerial()).isEqualTo("A001");
    }
}

thenReturn()

  • 스터빙한 메소드 호출 후 어떤 객체를 반환할 건지 정의합니다.
  • 파라미터에 메소드의 반환타입에 맞춰 반환 값을 지정해줍니다.

thenThrow()

  • 스터빙한 메소드 호출 후 어떤 Exception을 Throw할지 정의합니다.
  • 파라미터에 발생시킬 Exception을 지정해줍니다.

thenAnswer()

  • 스터빙한 메소드 호출 후 어떤 작업을 할지 custom하게 정의합니다.
  • mockito javadoc에서는 이 메소드를 사용하지 말고 위의 두 메소드를 사용할 것을 권장하고 있습니다.

thenCallRealMethod()

  • 실제 메소드를 호출합니다.

Stubber Method

ongoing 과 달리 반환타입이 void인 메소드를 테스트할 수 있습니다

// {Stubber 메소드}.when({스터빙할 클래스}).{스터빙할 메소드}

doThrow(new IllegalArgumentException()).when(memberService).validate(2L);

doReturn

  • thenReturn과 동일

doThrow

  • thenThrow와 동일

doAnswer

  • thenAnswer과 동일

doNothing

  • 스터빙 메소드 호출 후 어떤 행동도 하지 않게 정의합니다.

doCallRealMethod

  • thenCallRealMethod와 동일

Mock Object Verification 🔥

verify 메소드를 통해 스터빙한 메소드의 호출을 확인할 수 있습니다.

verify()

// verify(T mock, VerificationMode mode)

@ExtendWith(MockitoExtension.class)
public class VerifyMethod {
 
    @Mock
    UserService userService;
 
    @Test
    void testVerifyTimes() {
        userService.getUser();
        userService.getUser();
 
        verify(userService, times(2)).getUser();
    }
 
    @Test
    void testVerifyNever() {
        verify(userService, never()).getUser();
    }
 
    @Test
    void testAtLeastOne() {
        userService.getUser();
 
        verify(userService, atLeastOnce()).getUser();
    }
 
    @Test
    void testAtLeast() {
        //한 번만 실행하면 fail
        userService.getUser();
        userService.getUser();
        userService.getUser();
 
        verify(userService, atLeast(2)).getUser();
    }
 
    @Test
    void testAtMostOnce() {
        userService.getUser();
        // userService.getUser(); - 2번 이상 실행하면 fail
 
        verify(userService, atMostOnce()).getUser();
    }
 
    @Test
    void testAtMost() {
        userService.getUser();
        userService.getUser();
        userService.getUser();
        // userService.getUser(); - 6번 이상 실행하면 fail
 
        verify(userService, atMost(3)).getUser();
    }
 
    @Test
    void testOnly() {
        userService.getUser();
        // userService.getLoginErrNum(); - getUser()가 아닌 다른 메소드 실행 시 fail
 
        verify(userService, only()).getUser();
    }
}

VerificationMode

InOrder

  • 메소드 호출 순서를 검증합니다.
// inOrder(Mock Object명)으로 InOrder을 생성한 후 사용합니다.

@ExtendWith(MockitoExtension.class)
public class VerifyInOrderMethod {
 
    @Mock
    UserService userService;
 
    @Mock
    ProductService productService;
 
    @Test
    void testInOrder() {
 
        // 이 순서로 하면 fail
        // userService.getLoginErrNum();
        // userService.getUser();
        userService.getUser();
        userService.getLoginErrNum();
 
        InOrder inOrder = inOrder(userService);
 
        inOrder.verify(userService).getUser();
        inOrder.verify(userService).getLoginErrNum();
    }
 
    @Test
    void testInOrderWithCalls() {
 
        // 이 순서로 하면 fail
        // userService.getUser();
        // userService.getLoginErrNum();
        // userService.getUser();
 
        userService.getUser();
        userService.getUser();
        userService.getLoginErrNum();
 
        InOrder inOrder = inOrder(userService);
 
        inOrder.verify(userService, calls(2)).getUser();
        inOrder.verify(userService).getLoginErrNum();
    }
 
    @Test
    void testInOrderWithVerifyNoMoreInteractions() {
        userService.getUser();
        // userService.getLoginErrNum(); - 실행하면 fail
 
        InOrder inOrder = inOrder(userService);
 
        inOrder.verify(userService).getUser();
 
        verifyNoMoreInteractions(userService); //위에 verify 이후 userService를 호출하면 fail
    }
 
    @Test
    void testInOrderWithVerifyNoInteractions() {
        userService.getUser();
        userService.getLoginErrNum();
        // productService.getProduct(); - 실행하면 fail
        
        InOrder inOrder = inOrder(userService);
 
        inOrder.verify(userService).getUser();
        inOrder.verify(userService).getLoginErrNum();
 
        verifyNoInteractions(productService); //productService를 호출하면 fail
    }
}

verifyNoMoreInteractions(T mock)

  • 선언한 verify 후 해당 mock을 실행하면 실패합니다.

verifyNoInteractions(T mock)

  • 테스트 내에서 mock을 실행하면 실패합니다.

Argument Matchers

  • Method Stub을 할 때 메서드의 특정 인자값에 반응하게 하는게 아니라 좀 더 유연하게 인자값을 설정하고싶을 때 Argument matchers를 사용합니다.
  • any() , anyString() 등을 이용합니다.

Argument Captor

  • 특정 메소드에 사용되는 인자를 저장해놓았다가 사용할 수 있습니다.
  • 파라미터 타입으로 생성합니다.
  • verify와 함께 capture()를 통해 해당 메소드가 사용한 인자를 저장해둡니다.

@Captor

  • 테스트 필드에 선언하여 사용할 수 있습니다.
@Captor
ArgumentCaptor<Integer> integerArgumentCaptor;

@Test
void capture() throws Exception {
    MockitoAnnotations.initMocks(this);

    final List<String> mockedList = mock(List.class);
    when(mockedList.get(1)).thenReturn("A");
    when(mockedList.get(2)).thenReturn("B");
    when(mockedList.get(3)).thenReturn("C");

    assertThat(mockedList.get(1)).isEqualTo("A");
    assertThat(mockedList.get(3)).isEqualTo("C");
    assertThat(mockedList.get(2)).isEqualTo("B");

    verify(mockedList, times(3)).get(integerArgumentCaptor.capture());

    final List<Integer> allValues = integerArgumentCaptor.getAllValues();
    assertThat(allValues).isEqualTo(Arrays.asList(1, 3, 2));
}

ArgumentCaptor.forClass

@Test
void capture() throws Exception{
    // stubbing
    final List<String> mockedList = mock(List.class);
    when(mockedList.get(1)).thenReturn("A");
    when(mockedList.get(2)).thenReturn("B");
    when(mockedList.get(3)).thenReturn("C");

    // 1.
    ArgumentCaptor<Integer> integerArgumentCaptor = ArgumentCaptor.forClass(Integer.class);

    assertThat(mockedList.get(1)).isEqualTo("A");
    assertThat(mockedList.get(3)).isEqualTo("C");
    assertThat(mockedList.get(2)).isEqualTo("B");

    // 2.
    verify(mockedList, times(3)).get(integerArgumentCaptor.capture());
    // verify(mockedList).get(1);
    // verify(mockedList).get(2);
    // verify(mockedList).get(3);

    // 3.
    final List<Integer> allValues = integerArgumentCaptor.getAllValues();
    assertThat(allValues).isEqualTo(Arrays.asList(1, 3, 2));
}

Mockito BDD Style API 🔥

  • Behavior Driven Development (행위 기반 테스트)
  • Mockito는 BddMockito라는 클래스를 통해 BDD스타일의 API도 제공하고 있습니다.
  • 저는 개인적으로 이 방법을 주로 사용합니다.

BDD 설계 방법

Title

Narrative

  • As a
  • I want
  • so that

Acceptance Criteria

실제 테스트에선 이 부분을 사용합니다.

  • Given
    • 초기 context 값
  • When
    • 테스트하려는 조건
  • Then
    • 테스트 결과

기존 Mockito와의 차이점

when → given

  • when~ then~ 에서 given~ will~ 로 변경되었습니다.

verify → then

  • verify~ 에서 then~ should~ 로 변경되었습니다.

 

 

 

 

 

출처

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

https://effortguy.tistory.com

https://catsbi.oopy.io/a29ed4d0-49d3-44d3-bd24-154d6a7dc71f

댓글