Skip to content

Mockito

최원용 edited this page Apr 20, 2023 · 1 revision

Mockito

Mock 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 프레임워크

용어 정리

  • Mock
    • 진짜 객체와 비슷하지만 물리적으로 같지 않고 프로그래머가 직접 행동을 관리하는 객체이다.
    • Mock은 모든 상호작용을 기억한다. 사용자는 Mock의 어떤 메서드가 실행되었는지 선택적으로 검증할 수 있다.
  • Stubbing
    • 테스트 코드에서 Mock 객체를 사용할 때, Mock의 특정 메서드 호출과 응답을 정의하는 것을 말한다.
  • Spy
    • Mock 객체처럼 모든 상호작용을 기억하면서 객체의 원래 메서드도 호출할 수 있는 객체이다.

테스트를 작성하는 자바 개발자의 약 45%(JetBrains 설문조사)가 Mockito Framework를 사용한다. Mockito를 대체할 수 있는 테스트 프레임워크는 EasyMock, JMock 등이 있다.

왜 사용하는가?

  • 애플리케이션에서 구현하지 않은 메서드를 테스트하려고 할 때
  • 해당 클래스가 어떻게 동작하는지 항상 미리 생각하고 계산하며 테스트를 작성한다면 매우 불편할 것이다.
  • 이럴 때 미리 Mock 객체를 생성해서 사용하면 조금 더 편하게 테스트 할 수 있다.
  • ex) Member 또는 MemberDao가 어떻게 작동하는지 Mockito를 이용해 만들어 놓으면 Member와 MemberDao 객체를 구현하기 전에도 테스트를 작성할 수 있다.

이미 구현된 클래스를 모킹할 필요가 있을까?

  • 굳이 그럴 필요는 없다.
  • 하지만 개발자가 컨트롤하기 힘든 부분이나 아직 구현되지 않은 클래스모킹이 필요한 경우가 많다.

사용하기 전

  • 스프링부트 2.2+ 프로젝트 생성시 spring-boot-start-test에서 자동으로 Mockito를 추가해준다.
  • 스프링부트 2.2 버전 이하 또는 스프링부트를 사용하지 않는다면, 아래와 같이 의존성을 추가해준다.

Gradle Dependency 추가

testImplementation 'org.mockito:mockito-junit-jupiter:5.2.0'

어노테이션을 알아보기 전에! Verify란?

  • 테스트하고자 하는 메서드가 의도한 대로 동작하는지 검증하는 것을 말한다. (행위 검증)

메소드 호출 개수 검증

@DisplayName("stubbing한 객체의 메서드 호출 횟수를 검증")
@Test
void stubbing_object_method_call_count_validate() {
    // given
    List<String> mockedList = mock(List.class);

    // when
    mockedList.add("a");
    mockedList.add("a");

    // then
     verify(mockedList, times(2)).add("a");
}

메서드를 한번도 호출하지 않았는지 검증

@DisplayName("stubbing한 객체의 메서드를 한 번도 호출하지 않았는지 검증")
@Test
void stubbing_object_method_never_call() {
    // given
    List<String> mockedList = mock(List.class);

    // when
    mockedList.add("a");
    mockedList.add("b");

    // then
    verify(mockedList, never()).get(1);
}

메서드가 최소 한 번은 호출되었는지 검증

@DisplayName("stubbing한 객체의 메서드가 최소 한 번은 호출되었는지 검증")
@Test
void stubbing_object_method_call_count_at_least_once() {
    // given
    List<String> mockedList = mock(List.class);

    // when
    mockedList.add("a");
    mockedList.add("b");

    // then
    verify(mockedList, atLeastOnce()).add("a");
}

메서드가 최소 N번 호출되었는지 검증

@DisplayName("stubbing한 객체의 메서드가 최소 n은 호출되었는지 검증")
@Test
void stubbing_object_method_call_at_least_n() {
    // given
    List<String> mockedList = mock(List.class);

    // when
    mockedList.add("a");
    mockedList.add("a");
    mockedList.add("a");

    // then
    verify(mockedList, atLeast(3)).add("a");
}

메서드가 최대 1번 호출 되었는지 검증

@DisplayName("stubbing한 객체의 메서드가 최대 1번 호출되었는지 검증")
@Test
void stubbing_object_method_call_at_most_once() {
    // given
    List<String> mockedList = mock(List.class);

    // when, then
    mockedList.add("a");
    verify(mockedList, atMostOnce()).add("a");
 }
		
@DisplayName("stubbing한 객체의 메서드가 최대 1번 호출되었는지 검증")
@Test
void stubbing_object_method_call_at_most_once() {
    // given
    List<String> mockedList = mock(List.class);

    // when, then
    mockedList.add("a");
    verify(mockedList, atMostOnce()).add("a");
}

메서드가 최대 N번 호출 되었는지 검증

@DisplayName("stubbing한 객체의 메서드가 최대 n번 호출되었는지 검증")
@Test
void stubbing_object_method_call_at_most_n() {
    // given
    List<String> mockedList = mock(List.class);

    // when, then
    mockedList.add("a");
    mockedList.add("a");
    mockedList.add("a");
    verify(mockedList, atMost(3)).add("a");
}

메서드 호출 순서 검증(inOrder)

@DisplayName("inOrder로 stubbing한 객체의 메서드 호출 순서 검증")
@Test
void stubbing_object_method_calls_sequence() {
    // given
    List<String> mockedList = mock(List.class);
    InOrder inOrder = inOrder(mockedList);

    // when, then
    mockedList.add("a");
    mockedList.add("b");
    mockedList.add("c");

    inOrder.verify(mockedList).add("a");
    inOrder.verify(mockedList).add("b");
    inOrder.verify(mockedList).add("c");
}

메서드 호출 횟수 검증(inOrder)

@DisplayName("inOrder로 stubbing한 객체의 메서드 호출 횟수 검증")
@Test
void stubbing_object_method_calls_n() {
    // given
    List<String> mockedList = mock(List.class);
    InOrder inOrder = inOrder(mockedList);

    // when, then
    mockedList.add("a");
    mockedList.add("b");
    mockedList.add("c");

    inOrder.verify(mockedList, calls(1)).add("a");
    inOrder.verify(mockedList, calls(1)).add("b");
    inOrder.verify(mockedList, calls(1)).add("c");
}

해당 검증 메서드만 실행되었는지 검증

@DisplayName("해당 검증 메소드만 실행됐는지 검증")
@Test
void stubbing_object_method_call_only() {
    // given
    List<String> mockedList = mock(List.class);

    // when, then
    mockedList.add("a");
 // mockedList.add("a")와 다른 메서드가 있다면 fail
 // mockedList.add("b");
 // mockedList.add("c");

    verify(mockedList, only()).add("a");
}

해당 검증 메서드 실행시간 검증

@DisplayName("해당 검증 메서드 실행시간 검증")
@Test
void stubbing_object_method_call_time_out() {
    // given
    List<String> mockedList = mock(List.class);

    // when, then
    mockedList.add("a");

    verify(mockedList, timeout(1)).add("a");
}

When-Then

When_Then을 이용하여, mock 객체의 메서드를 원하는 방식으로 정의할 수 있다.

  • 반환값이 존재하는 메서드를 stubbing
@Test
@DisplayName("반환값이 존재하는 메서드를 stubbing - when_thenReturn")
void whenConfigNonVoidReturnMethodStub1() {
    List<Integer> listMock = mock(List.class);
    when(listMock.get(anyInt())).thenReturn(STUB_RETURN_VALUE);

    final Integer getValue = listMock.get(SECURE_RANDOM.nextInt());
    assertThat(getValue)
            .isEqualTo(STUB_RETURN_VALUE);
}

위 처럼 when_then을 이용하여, mock 메서드의 반환 값을 간단하게 정의할 수 있다.

  • 다른 방식으로 반환값이 존재하는 메서드를 stubbing
@Test
@DisplayName("반환값이 존재하지 않는 메서드를 stubbing - doReturn_when")
void whenConfigNonVoidReturnMethodStub2() {
    List<Integer> listMock = mock(List.class);
    doReturn(STUB_RETURN_VALUE).when(listMock).get(anyInt());

    final Integer getValue = listMock.get(SECURE_RANDOM.nextInt());
    assertThat(getValue)
            .isEqualTo(STUB_RETURN_VALUE);
}
  • 반환값이 존재하지 않는 메서드를 stubbing
/*
void add(int index, E element)
 */
@Test
@DisplayName("반환값이 존재하지 않는 메서드를 stubbing - doNothing_when")
void whenConfigVoidReturnMethodStub() {
    ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
    List<Integer> listMock = mock(List.class);

    doNothing().when(listMock).add(anyInt(), captor.capture());

    listMock.add(SECURE_RANDOM.nextInt(), CAPTOR_ARGUMENT_VALUE);

    assertThat(captor.getValue())
            .isEqualTo(CAPTOR_ARGUMENT_VALUE);
}
  • 반환값이 존재하는 메서드에 여러 반환값을 stubbing
@Test
@DisplayName("반환값이 존재하는 메서드에 여러개의 반환값을 stubbing - chaining")
void whenConfigNonVoidReturnMethodStubMultiValue() {
    List<Integer> listMock = mock(List.class);
    when(listMock.get(anyInt()))
            .thenReturn(STUB_RETURN_VALUE)
            .thenThrow(CustomException.class);

    assertThat(listMock.get(SECURE_RANDOM.nextInt()))
            .isEqualTo(STUB_RETURN_VALUE);
    assertThatThrownBy(() -> listMock.get(SECURE_RANDOM.nextInt()))
            .isInstanceOf(CustomException.class);
}

when에 여러 then… 메서드를 체이닝하여 다양한 값이 반환되도록 설정할 수 있다.

설정한 값들보다 더 많이 메서드를 호출하면, 가장 마지막 값이 반복되어 반환된다.

@Test
@DisplayName(
"반환값이 존재하는 메서드에 여러개의 반환값을" + 
"stubbing - chaining 설정값보다 더 많이 호출할 때")
void whenConfigNonVoidReturnMethodStubMultiValue2() {
    List<Integer> listMock = mock(List.class);
    when(listMock.get(anyInt()))
            .thenReturn(STUB_RETURN_VALUE)
            .thenThrow(CustomException.class);

    assertThat(listMock.get(SECURE_RANDOM.nextInt()))
            .isEqualTo(STUB_RETURN_VALUE);
    assertThatThrownBy(() -> listMock.get(SECURE_RANDOM.nextInt()))
            .isInstanceOf(CustomException.class);
    assertThatThrownBy(() -> listMock.get(SECURE_RANDOM.nextInt()))
            .isInstanceOf(CustomException.class);
    assertThatThrownBy(() -> listMock.get(SECURE_RANDOM.nextInt()))
            .isInstanceOf(CustomException.class);
}
  • mock객체의 실제 메서드를 호출할 때
@Test
@DisplayName("Mock 객체의 실제 메서드 호출")
void whenConfigCallRealMethod() {
    final TestList listMock = mock(TestList.class);

    when(listMock.size()).thenCallRealMethod();

    assertThat(listMock.size())
            .isEqualTo(TestList.CUSTOM_SIZE);
}

static class TestList extends AbstractList<Integer> {

    public static final int CUSTOM_SIZE = 20;

    @Override
    public Integer get(final int index) {
        return -1;
    }

    @Override
    public int size() {
        return CUSTOM_SIZE;
    }
}
  • 메서드에 customAnswer를 지정
@Test
@DisplayName("메서드에 Custom Answer 지정")
void whenConfigMethodCustomAnswer() {
    final int randomArgument = SECURE_RANDOM.nextInt();
    List<Integer> listMock = mock(List.class);
    doAnswer(invocation -> invocation.getArgument(0)).when(listMock).get(anyInt());

    final Integer returnValue = listMock.get(randomArgument);
    assertThat(returnValue)
            .isEqualTo(randomArgument);
}

doAnswer를 이용하여, CustomAnswer를 만들어 지정할 수 있다. 함수형 인터페이스 형태기에, 람다로도 작성가능하다.

public interface Answer<T> {
    /**
     * @param invocation the invocation on the mock.
     *
     * @return the value to be returned
     *
     * @throws Throwable the throwable to be thrown
     */
    T answer(InvocationOnMock invocation) throws Throwable;
}

Mock Annotation

@Mock

Mockito.mock() 을 사용하지 않고 mock 된 인스턴스를 만들 수 있다.

  • mock된 인스턴스의 내부는 비어있 다.
  • mock 객체를 만드는 방법은 아래와 같다.

어노테이션을 사용하는 방법

@Mock
ArrayList<String> mockedList;

직접 선언하는 방법

ArrayList<String> mockedList = mock(List.class);
@Mock
ArrayList<String> mockedList;

@DisplayName("목 객체를 스터빙하여 테스트한다. ( @Mock 사용 )")
@Test
public void useMockAnnotation() {
		//given
    when(mockedList.add("one")).thenReturn(false);
		
		//when
    final boolean result = mockedList.add("one");

    //then
    verify(mockedList).add("one");
    Assertions.assertThat(result).isFalse();
}

@Spy

Mockito.spy()를 사용하지 않고 기존 인스턴스를 spy할 수 있다.

  • spy를 사용한 인스턴스는 기존 인스턴스와 동일하다.
  • 원하는 부분만 stubbing 할 수 있다.
  • spy를 객체를 만드는 방법은 아래와 같다.

어노테이션을 사용하는 방법

@Spy
ArrayList<String> spyList;

직접 선언하는 방법

ArrayList<String> spyList = Mockito.spy(List.class);
@Spy
ArrayList<String> spyList;

@DisplayName("Spy한 객체를 테스트한다. ( @Spy 사용 )")
@Test
public void useMockAnnotation() {
		//given
    when(spyList.add("one")).thenReturn(false);
		
		//when
    final boolean stubResult = spyList.add("one");
    final boolean originalResult = spyList.add("two");
		
		//then
	  //메서드 호출을 검증한다.
    verify(spyList).add("one");
    verify(spyList).add("two");

    Assertions.assertThat(stubResult).isFalse();
    Assertions.assertThat(originalResult).isTrue();
}

@Captor

메서드에 전달된 인자를 캡처하는 기능을 제공한다.

  • 캡처할 인자의 타입에 해당하는 Captor 객체를 생성해야 한다.
  • verify 메서드로 메서드의 인자를 캡처할 수 있다.

어노테이션을 사용하는 방법

@Captor
final ArgumentCaptor<String> args;

직접 선언하는 방법

ArgumentCaptor<List> listArgumentCaptor = ArgumentCaptor.forClass(List.class);
@Captor
final ArgumentCaptor<String> args;

@DisplayName("ArgumentCaptor를 사용하여 메서드 호출에 사용된 인자를 저장하여 검증한다.(호출을 한 번 했을 때 )")
@Test
public void useArgumentCaptorOnce() {
    // given
    final List<String> mockList = mock(List.class);

		// when
    mockList.add("first");
    verify(mockList).add(args.capture());

		// then
    Assertions.assertThat(args.getValue()).isEqualTo("first");
}

Mock 객체가 주입되어 내부적으로 객체의 메서드가 호출되는 경우에도 인자를 캡쳐할 수 있다.

CaptorOuterFixture

public class CaptorOuterFixture {

    private CaptorInnerFixture captorInnerFixture;

    public CaptorOuterFixture(final CaptorInnerFixture captorInnerFixture) {
        this.captorInnerFixture = captorInnerFixture;
    }

    public void show(final String name, final int id, final boolean isExist) {
        final CaptorMessageFixture captorMessageFixture = new CaptorMessageFixture(name, id, isExist);
        captorInnerFixture.print(captorMessageFixture);
    }
}

CaptorInnerFixture

public class CaptorInnerFixture {

    public void print(final CaptorMessageFixture captorMessageFixture) {
        System.out.println(captorMessageFixture);
    }
}

CaptorMessageFixture

public class CaptorMessageFixture {

    private final String name;
    private final int number;
    private final boolean isExist;

    public CaptorMessageFixture(final String name, final int number, final boolean isExist) {
        this.name = name;
        this.number = number;
        this.isExist = isExist;
    }

    public String getName() {
        return name;
    }
}
  • Test Code
@DisplayName("내부 동작의 인자 또한 ArgumentCaptor로 캡쳐할 수 있다.")
@Test
void captureInnerParameter() {
    //given
    final CaptorInnerFixture innerFixture = mock(CaptorInnerFixture.class);
    final CaptorOuterFixture captorOuterFixture = new CaptorOuterFixture(innerFixture);
    final ArgumentCaptor<CaptorMessageFixture> messageCaptor = ArgumentCaptor.forClass(CaptorMessageFixture.class);

    //when
    captorOuterFixture.show("메시지", 1, true);

    //then
    verify(innerFixture).print(messageCaptor.capture());
    final CaptorMessageFixture message = messageCaptor.getValue();
    Assertions.assertThat(message.getName()).isEqualTo("메시지");
}

@InjectMock

해당 객체의 멤버 변수로 존재하는 의존된 다른 객체들이 mock혹은 spy로 생성된 객체라면 의존성 주입을 해주는 기능을 제공한다.

  • 간단한 코드로 살펴보자.
  • Name이라는 객체를 컴포지션 관계로 가지고 있는 Car 객체다.
public class Car {

    private final Name name;

    public Car(Name name) {
        this.name = name;
    }

    public boolean checkCar(String name) {
        return this.name.isEqualsName(name);
    }

}
  • field로 가지고 있는 name과 파라미터로 받은 name이 같은지 검증하는 메서드를 가지고 있다.
public class Name {

    private final String name;

    public Name(String name) {
        this.name = name;
    }

    public boolean isEqualsName(String name) {
        return this.name.equals(name);
    }

}
  • InjectMocks 어노테이션을 사용하지 않는다면 아래와 같이 Mock 객체를 직접 생성자에 넣어 Mock 객체가 주입된 Car 객체를 만들어야 한다.
@Mock
private Name name;

@DisplayName("InjectMocks를 사용하지 않고 Mock 의존성을 주입받는 방법")
@Test
void non_inject_mocks() {
    // given
    final Car car = new Car(name);

    // when
    Mockito.when(car.checkCar("hyundai")).thenReturn(true);

    // then
    Assertions.assertThat(car.checkCar("hyundai")).isTrue();
    Assertions.assertThat(car.checkCar("kia")).isFalse();
}
  • InjectMocks 어노테이션을 사용한다면 따로 Car를 생성할 때 의존성을 주입하지 않아도 Mock이나 Spy 객체로 생성된 객체가 있다면 의존성을 주입해 준다.
@Mock
private Name name;

@InjectMocks
private Car car;

@DisplayName("InjectMocks를 사용해서 Mock 의존성을 주입받는 방법")
@Test
void inject_mocks() {
    // when
    Mockito.when(car.checkCar("hyundai")).thenReturn(true);

    // then
    Assertions.assertThat(car.checkCar("hyundai")).isTrue();
    Assertions.assertThat(car.checkCar("kia")).isFalse();
}

Mocking Exception

반환값이 존재할 때

  • When(mock.method).ThenThrow(Exception.class)로 메서드 실행시 Exception이 발생하도록 스터빙 할 수 있다.
@DisplayName("반환값이 존재하는 메서드를 Exception Stubbing 할 때 - class")
@Test
void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown() {
    List<String> strList = mock(List.class);
    when(strList.size()).thenThrow(CustomException.class);
    assertThatThrownBy(strList::size)
            .isInstanceOf(CustomException.class);
}

반환값이 존재하지 않을 때

  • doThrow(Exception.class).when(mock).method 로 메서드 실행시 Exception이 발생하도록 스터빙할 수 있다.
@DisplayName("반환값이 존재하지 않는 메서드를 Exception Stubbing 할 때 - class")
@Test
void whenConfigVoidReturnMethodToThrowEx_thenExIsThrown() {
    List<String> strList = mock(List.class);
    doThrow(CustomException.class).when(strList)
            .clear();
    assertThatThrownBy(strList::clear)
            .isInstanceOf(CustomException.class);
}

예외 객체를 넘기기

  • 이전에 사용했던 when.thenThrowdoThrow.when.method 에서 예외 객체 자체를 넘길 수도 있다.
  • 예외 객체를 생성해 넘김으로써 에러 메시지를 정의해줄 수 있다.
@DisplayName("반환값이 존재하는 메서드를 Exception Stubbing 할 때 - object")
@Test
void whenConfigNonVoidReturnMethodToThrowExWithNewExObj_thenExIsThrown() {
    List<String> strList = mock(List.class);
    when(strList.size()).thenThrow(new CustomException(EXCEPTION_MESSAGE));
    assertThatThrownBy(strList::size)
            .isInstanceOf(CustomException.class)
            .hasMessage(EXCEPTION_MESSAGE);
}

@DisplayName("반환값이 존재하지 않는 메서드를 Exception Stubbing 할 때 - object")
@Test
void whenConfigVoidReturnMethodToThrowExWithNewExObj_thenExIsThrown() {
    List<String> strList = mock(List.class);
    doThrow(new CustomException(EXCEPTION_MESSAGE)).when(strList)
            .clear();
    assertThatThrownBy(strList::clear)
            .isInstanceOf(CustomException.class)
            .hasMessage(EXCEPTION_MESSAGE);
}

Mockito ArgumentMatcher

  • mocked 메서드를 다양한 방식으로 설정할 수 있다.
  • 가장 간단하게 메서드 파라미터에 상수를 넣어 mock 메서드를 설정할 수 있다.
doReturn(STUBED_VALUE).when(mockedList).get(0);
  • 하지만 위와 같은 방법은 오로지 mockedList.get(0)에만 대응하는 방법이다.
  • 우리는 좀 더 넓은 범위의 파라미터를 정의하거나, 일반적인 상황에 대해 정의할 필요가 있다.
  • ArgumentMatcher를 이용하여, 위와같은 문제를 해결할 수가 있다.
 doReturn(STUBED_VALUE).when(mockedList).get(anyInt());
  • 파라미터 부분에 ArgumentMatchers.anyInt()를 넣어주면, 모든 Int값이 들어오는 경우에 대하여 Mocked 메서드를 정의할 수 있다.
  • anyInt외에도한 ArgumentMatchers가 정의 되어있다.
    • anyString()
    • anyFloat()
    • anyList()

와 같은 일반적으로 사용되는 타입 뿐 아니라 any(Class<T> classType) 이라는 메서드로 개인이 정의한 객체 또한 ArgumentMatchers로 이용할 수 있다.

아래는 ArugmentMatchers를 사용한 예시다.

  • Mockito는 equals를 사용하여 검증하는데 한 개가 아닌 여러 개의 유연한 검증이 필요할 때 사용된다.
  • ArgumentTestFixture
public class ArgumentTestFixture {

    public String parseToEnglish(final String korean) {
        return null;
    }
}
  • Test code
@Mock
private ArgumentTestFixture argumentTestFixture;

@DisplayName("인자에 anyString() 을 사용할 경우 어느 string 이 와도 같은 결과로 stubbing 된다.")
@Test
void useArgumentMatcherAnyInt() {
    when(argumentTestFixture.parseToEnglish(anyString())).thenReturn("one");

    final String oneResult = argumentTestFixture.parseToEnglish(anyString());
    final String twoResult = argumentTestFixture.parseToEnglish("일");
    final String thirdResult = argumentTestFixture.parseToEnglish("하나");

    verify(argumentTestFixture, times(3)).parseToEnglish(anyString());

    Assertions.assertThat(oneResult).isEqualTo("one");
    Assertions.assertThat(twoResult).isEqualTo("one");
    Assertions.assertThat(thirdResult).isEqualTo("one");
}

Mockito learning Test

https://github.com/wonyongChoi05/mockito-study

Clone this wiki locally