Mockito 란?

Mockito

Mockito(모키토)는 Java 테스트를 통해 Mock(모의) 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 프레임워크이다.

Mock은 진짜 객체와 비슷하게 동작하지만, 프로그래머가 직접 행동을 관리하는 객체이다.

  • FIRST 원칙 중에 Isolated 원칙을 지킬 수 있게 해준다.
  • 지정된 클래스의 모의를 작성하고, 임의의 메서드를 stub[^stub]으로 지정된 값을 돌려주도록 하거나, 모의의 메서드가 기대했던 대로 불려 갔는지 어떤지를 검증할 수 있다.
  • staticfinal 메서드의 Mock으로 만들 수 있다. (예전에는 할 수 없었는데, 언제부터인가 할 수 있게 되었다.)
  • 모의 동작의 정의하는 형태로 안전하게 정의할 수 있는 것이 큰 특징이다.

stub(스텁)은 테스트 중인 모듈이 의존하는 소프트웨어 구성 요소의 동작을 시뮬레이션하는 프로그램이다.

HelloWorld

mockito 빌드 파일 작성

plugins {
    id 'java'
}

group 'com.devkuma'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "org.mockito:mockito-core:4.8.0"
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

test {
    useJUnitPlatform()
}

(1) mockito 라이브러리 지정

testImplementation "org.mockito:mockito-core:4.8.0"

mockito 테스트 코드 작성

package com.devkuma.mockito;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.List;

import org.junit.jupiter.api.Test;

public class HelloMockitoTest {

    @Test
    void test() {
        List<String> mock = mock(List.class);

        when(mock.get(0)).thenReturn("Hello Mockito!");

        System.out.println(mock.get(0));
    }
}

실행 결과:

Hello Mockito!

(1) Mock 객체를 생성

List<String> mock = mock(List.class);
  • Mockito#mock(Class<?>)으로 지정된 유형의 Mock 객체를 생성한다.
  • 여기서는 java.util.List 모의 객체를 생성하였다.

(2) Mock 객체를 생성

when(mock.get(0)).thenReturn("Hello Mockito!");
  • Mockito#when(T)와 같이 API를 사용하여, 모의 동작을 정의한다.
  • 여기서는 Mock get(0) 메서드를 실행하면 "Hello Mockito!" 문자열을 반환하도록 정의하였다.
    • when() 메서드의 인수로 스텁으로 만들고 싶은 메서드 호출을 지정하고, 실행하도록 구현한다.
    • thenReturn(T)으로 when() 메서드의 인수로 실행하는 메서드가 반환하는 값을 지정한다.
    • when(T)이 리턴되는 데이터 타입은 thenReturn(T)에 지정한 데이터 타입이 동일해야 한다. 데이터 타입이 맞지 않으면 컴파일 에러가 발생한다.

Mock의 기본 동작

package com.devkuma.mockito;

import static org.mockito.Mockito.mock;

import java.util.List;

import org.junit.jupiter.api.Test;

public class DefaultReturnValueTest {

    @Test
    void test() {
        MockTest mock = mock(MockTest.class);

        System.out.println("mock.anyObject()        = " + mock.anyObject());
        System.out.println("mock.primitive()        = " + mock.primitive());
        System.out.println("mock.primitiveWrapper() = " + mock.primitiveWrapper());
        System.out.println("mock.array()            = " + mock.array());
        System.out.println("mock.collection()       = " + mock.collection());
    }

    public class MockTest {

        public int primitive() {
            return 1;
        }

        public Integer primitiveWrapper() {
            return 2;
        }

        public int[] array() {
            return new int[] {1, 2, 3};
        }

        public String anyObject() {
            System.out.println("Hello World");
            return "Hello World";
        }

        public List<String> collection() {
            return List.of("hello", "world");
        }
    }
}

실행 결과:

mock.primitive()        = 0
mock.primitiveWrapper() = 0
mock.collection()       = []
mock.array()            = null
mock.anyObject()        = null
  • Mock 메서드는 본래의 처리를 일절 실행하지 않게 된다.
  • 모의 메서드는 다음과 같이 값을 반환한다.
    • 일반 타입, 랩퍼 타입
      • 기본값
      • int0, booleanfalse
    • 컬렉션 타입
      • 빈 컬렉션
    • 배열, 기타 객체
      • null

Stub 만들기 : when

아래와 같이 형식으로 Mock 메서드가 반환하는 값을 지정할 수 있다.

when(<스텁으로 만들고 싶은 메서드 호출>).thenReturn(<메서드 반환값>)
package com.devkuma.mockito;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.List;

import org.junit.jupiter.api.Test;

public class WhenAndThenReturnTest {
    @Test
    void test() {
        List<String> mock = mock(List.class);

        when(mock.get(0)).thenReturn("hello");
        when(mock.get(1)).thenReturn("world");

        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(0) = " + mock.get(0));
        System.out.println("mock.get(1) = " + mock.get(1));
        System.out.println("mock.get(2) = " + mock.get(2));

        when(mock.get(1)).thenReturn("Override");
        System.out.println("mock.get(1) = " + mock.get(1));
    }
}

실행 결과:

mock.get(0) = hello
mock.get(0) = hello
mock.get(0) = hello
mock.get(1) = world
mock.get(2) = null
mock.get(1) = Override
  • 한 번 Stub으로 된 메서드는 항상 동일한 값을 반환한다.
  • Stub으로 된 인수도 조건에 포함되어 있으므로, 스텁이 되었을 때는 다른 인수로 메서드를 실행했을 경우는 지정된 값은 반환되지 않는다.
  • Stub은 덮어쓸 수 있으며, 동일한 조건에서 스텁을 재정의하면 새로 지정된 값을 반환한다.

실행할 때마다 다른 값을 반환하도록 Stub 만들기

인수가 가변 길이 인수 thenReturn(T...)를 사용하면 실행할 때마다 메서드가 반환하는 값이 변경되도록 Stub으로 만들 수 있다.

@Test
void testConsecutiveCalls() {
    List<String> mock = mock(List.class);

    when(mock.get(0)).thenReturn("one", "two", "three");

    System.out.println("mock.get(0) = " + mock.get(0));
    System.out.println("mock.get(0) = " + mock.get(0));
    System.out.println("mock.get(0) = " + mock.get(0));
    System.out.println("mock.get(0) = " + mock.get(0));
}

실행 결과:

mock.get(0) = one
mock.get(0) = two
mock.get(0) = three
mock.get(0) = three
  • 지정된 수 이상 메서드가 실행되었을 경우는 마지막으로 지정한 값이 계속 반환된다.

Exception를 발생하도록 Stub 만들기

thenThrow(Throwable)를 사용하면 메서드를 실행할 때 예외를 발생하도록 Stub으로 만들 수 있다.

@Test
void testThrowException() {
    List<String> mock = mock(List.class);

    when(mock.get(0)).thenThrow(new RuntimeException("test"));

    try {
        mock.get(0);
    } catch (Throwable e) {
        System.out.println("e = " + e);
    }
}

실행 결과:

e = java.lang.RuntimeException: test

실행할 때마다 다른 예외를 하도록 Stub 만들기

인수가 가변 길이 인수로 thenThrow(Throwable...)를 사용하면 실행할 때마다 메서드가 발생하는 예외가 변경되도록 스텁을 만들 수 있다.

@Test
void testConsecutiveThrowException() {
    List<String> mock = mock(List.class);

    when(mock.get(0)).thenThrow(
            new RuntimeException("one"),
            new NullPointerException("two"),
            new IllegalArgumentException("three")
    );
    try {
        mock.get(0);
    } catch (Throwable e1) {
        System.out.println("e1 = " + e1);
    }
    try {
        mock.get(0);
    } catch (Throwable e2) {
        System.out.println("e2 = " + e2);
    }
    try {
        mock.get(0);
    } catch (Throwable e3) {
        System.out.println("e3 = " + e3);
    }
    try {
        mock.get(0);
    } catch (Throwable e4) {
        System.out.println("e4 = " + e4);
    }
}

실행 결과:

e1 = java.lang.RuntimeException: one
e2 = java.lang.NullPointerException: two
e3 = java.lang.IllegalArgumentException: three
e4 = java.lang.IllegalArgumentException: three
  • 지정된 수 이상 메서드가 실행되면, 마지막으로 지정한 예외를 계속 발생하게 된다.

반환값과 예외를 던지도록 조합하여 Stub 만들기

thenReturn(), thenThrow() 메서드 체인을 연결하여 반환 값을 지정하고, 예외를 발생하도록 결합하여 Stub을 만들 수 있다.

@Test
void testConsecutiveThrowAndReturn() {
    List<String> mock = mock(List.class);

    when(mock.get(0))
        .thenReturn("one")
        .thenThrow(new RuntimeException("two"))
        .thenReturn("three");

    System.out.println("mock.get(0) = " + mock.get(0));
    try {
        mock.get(0);
    } catch (Throwable e) {
        System.out.println("e = " + e);
    }
    System.out.println("mock.get(0) = " + mock.get(0));
}

실행 결과:

mock.get(0) = one
e = java.lang.RuntimeException: two
mock.get(0) = three
  • Stub의 정의는 덮어 쓰기되므로, 체인으로 하지 않고 개별적으로 정의하면 마지막 정의만 유효하게 되므로 주의해야 한다.
    • 아래와 같이 정의하면 마지막 thenReturn("three")만 유효하게 된다.
      when(mock.get(0)).thenReturn("one"); // 덮여 쓰여진다.
      when(mock.get(0)).thenThrow(new RuntimeException("two")); // 덮여 쓰여진다.
      when(mock.get(0)).thenReturn("three"); // 이것만 유효하다.
      

임의의 인수로 Stub 만들기

인수로 ArgumentMatchers으로 정의되어 있는 any로 시작하는 메서드(anyInt(),anyList(), any() 등)를 사용하여 스텁을 정의하면, 임의의 인수에서 메서드 실행을 stub을 만들 수 있다.

@Test
void testAnyArgumentMatch() {
    List<String> mock = mock(List.class);

    when(mock.get(anyInt())).thenReturn("test");

    System.out.println("mock.get(0) = " + mock.get(0));
    System.out.println("mock.get(1) = " + mock.get(1));
    System.out.println("mock.get(2) = " + mock.get(2));
}

실행 결과:

mock.get(0) = test
mock.get(1) = test
mock.get(2) = test

ArgumentMachers에서 제공하는 매치용의 메서드

ArgumentMatchers에는 자주 사용하는 매치용의 메서드가 제공되고 있다.

anyInt() 또는 anyCollection()와 같은 any타입()null 이외에 타입명의 타입과 일치하는 임의의 값과 일치한다.

아래에서는 any타입() 이외의 메서드에 대해 매치의 조건을 정리한다. (eq()~That()는 사용하는 곳이 조금 다르니 이후에 설명하도록 하겠다.)

메서드 설명
startsWith(String) 지정된 문자열로 시작한다.
endsWith(String) 지정된 문자열로 끝난다.
contains(String) 지정한 문자열 포함한다.
matches(String) 지정된 정규 표현식과 일치한다.
isNull() 인수로 null이 있다.
isNotNull(), notNull() 인수로 null이 없다.
any() null를 포함한 모든 값과 일치한다.
any(Class<T>), isA(Class<T>) null 이외의 지정된 형태의 임의의 값에 매치한다.
nullable(Class<T>) null를 포함하여 지정된 형식의 모든 값과 일치한다.
same(T) 지정된 객체와 동일한 객체인지 여부를 판단한다.

여러 인수 중 일부에만 매칭 적용

여러 인수 중에 일부만 매칭을 적용을 어떻게 해야 하는지 알아보도록 하겠다.

일부만 매칭 적용하면 에러

아래 예제에서 두 번째 인수만 임의의 문자열(anyString())로 하려고 한다.

@Test
void testStubbingMultipleArgumentsMethod_invalidCase() {
    BiFunction<String, String, String> mock = mock(BiFunction.class);

    when(mock.apply("hello", anyString())).thenReturn("mocked");

    System.out.println(mock.apply("hello", "world"));
}

실행을 하면, 아래와 같이 오류 메세지가 표시된다.


Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at com.devkuma.mockito.WhenTest.testStubbingMultipleArgumentsMethod_invalidCase(WhenTest.java:120)

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(any(), eq("String by matcher"));

For more info see javadoc for Matchers class.

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at com.devkuma.mockito.WhenTest.testStubbingMultipleArgumentsMethod_invalidCase(WhenTest.java:120)

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(any(), eq("String by matcher"));

... 이하 생략 ...
  • 인수가 여러개인 메서드를 Stub으로 만들려고 할 시에는 하나라도 매칭으로 조건을 설정하게 되면, 다른 모든 인수도 마찬가지로 매칭으로 조건을 설정하지 않으면 위에 같은 에러가 발생하게 된다.
일부만 매칭 적용하는 방법

인수가 여러개인 메서드를 Stub으로 만들려면, 동일한 모두 매칭 조건을 설정을 해주면 된다.

@Test
void testStubbingMultipleArgumentsMethod_validCase() {
    BiFunction<String, String, String> mock = mock(BiFunction.class);

    when(mock.apply(eq("hello"), anyString())).thenReturn("mocked");

    System.out.println(mock.apply("hello", "world"));
}

실행 결과:

mocked
  • 위와 같이 ArgumentMatcherseq() 메서드를 사용하여 첫 번째 인수도 일치 형식으로 조건을 설정하면 오류가 발생하지 않는다.

사용자 정의 매칭 처리 지정

ArgumentMatchersargThat(ArgumentMatcher)을 사용하면 매치 처리를 구현하는 모든 ArgumentMatcher를 사용하여 조건을 지정할 수 있다.

@Test
void testCustomArgumentMatcher() {
    Function<String, String> mock = mock(Function.class);

    ArgumentMatcher<String> isFoo = arg -> arg.equals("hello");

    when(mock.apply(argThat(isFoo))).thenReturn("HELLO!!!");

    System.out.println("mock.apply(hello) = " + mock.apply("hello"));
    System.out.println("mock.apply(hey) = " + mock.apply("hey"));
}

실행 결과:

mock.apply(devkuma) = HELLO!!!
mock.apply(araikuma) = null

Stub으로 만든 메서드 내에서 임의의 처리를 실행

thenAnswer(Answer)를 사용하면 Stub으로 만든 메서드가 호출될 때 수행할 작업을 지정할 수 있다.

@Test
void testAnswer() {
    Function<String, String> mock = mock(Function.class);

    Answer<String> answer = invocation -> {
        System.out.println("arguments = " + Arrays.toString(invocation.getArguments()));
        return "world";
    };

    when(mock.apply("hello")).thenAnswer(answer);

    System.out.println("mock.apply(hello) = " + mock.apply("hello"));
    System.out.println("mock.apply(hey)   = " + mock.apply("hey"));
}

실행 결과:

arguments = [hello]
mock.apply(hello) = world
mock.apply(hey)   = null
  • Stub 처리는 Answer으로 구현한다.
    • answer(InvocationOnMock) 메서드를 구현한다.
    • 인수로 받는 InvocationOnMock으로 실제로 전달된 인수를 받아 올 수 있다.

인수를 타입을 다양하게 Answer1 ~ Answer6

  • Answer1, Answer2, Answer3와 같은 인터페이스가 제공되고 있어 인수를 타입을 다양하게 받고 싶은 경우에 이용할 수 있다.
package com.devkuma.mockito;

import static org.mockito.AdditionalAnswers.answer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.function.Function;

import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer1;

public class StubbingTest {

    @Test
    void testAnswer1() {
        Function<String, String> mock = mock(Function.class);

        Answer1<String, String> answer1 = arg -> {
            System.out.println("arg = " + arg);
            return "world";
        };

        when(mock.apply("hello")).thenAnswer(answer(answer1));

        System.out.println("mock.apply(hello) = " + mock.apply("hello"));
        System.out.println("mock.apply(hey)   = " + mock.apply("hey"));
    }
}

실행 결과:

arg = hello
mock.apply(hello) = world
mock.apply(hey)   = null
  • AdditionalAnswersanswer()Answer1~Answer6을 전달하여 사용한다.
  • Answer1 등으로 InvocationOnMock으로가 아닌, 전달된 인수를 직접받을 수 있다.

반환값이 void의 메서드를 Stub 만들어 (예외가 발생시키도록) 한다

@Test
void testReturnVoid() {
    Runnable mock = mock(Runnable.class);

    doThrow(new RuntimeException("test")).when(mock).run();

    try {
        mock.run();
    } catch (Throwable e) {
        System.out.println("e = " + e);
    }
}

실행 결과:

e = java.lang.RuntimeException: test
  • when(<스텁으로 만들고 싶은 메서드 호출>)으로 하는 방법은 Stub으로 만들려는 메서드가 반환 값을 가지고 있다고 가정한다.
    • 반환값이 void 메서드라면, when()에 전달해도 컴파일 에러가 발생한다.
  • 반환값이 void의 메서드가 예외가 발생하도록 Stub을 만들고 싶은 경우는 doThrow()로부터 시작되는 스텁화의 방법을 이용한다.
    • doThrow()의 인수에 발생하고 싶은 예외를 설정한다.
    • 이어서 when()인수에 모의 객체를 전달한다.
    • when()의 반환값의 형태는 인수로 건네준 Mock 객체와 같은 형태가 되어 있으므로 Stub으로 만들고 싶은 메서드 호출을 메서드 체인으로 계속해서 실행한다.

메서드가 호출되었는지 확인

Mockito#verify(T) 메서드를 사용하면, Mock 메서드가 실행되었는지 여부를 확인할 수 있다.

  • verify() 메서드의 인수로 Mock 객체를 전달한다.
  • verify() 메서드의 반환값 형태는 인수로 전달된 Mock 객체와 같은 형태가 되어 있어, 메서드 체인으로 메서드를 실행하여 그 메서드가 실행되었는지 여부를 검증할 수 있다.
  • 검증은 인수를 포함하여 수행된다.
package com.devkuma.mockito;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.util.List;

import org.junit.jupiter.api.Test;

public class VerifyTest {

    @Test
    void test() {
        List<String> mock = mock(List.class);

        mock.get(1);

        verify(mock).get(0);
    }
}

실행 결과:

Argument(s) are different! Wanted:
list.get(0);
-> at com.devkuma.mockito.VerifyTest.test(VerifyTest.java:18)
Actual invocations have different arguments:
list.get(1);
-> at com.devkuma.mockito.VerifyTest.test(VerifyTest.java:16)

위의 예제에서는 get(0)가 한 번만 실행되었는지 확인하였다.

  • 한 번도 실행되지 않아 오류가 발생한다.
  • get(0)가 두 번 실행되는 경우에도 오류가 발생한다.

인수 매칭

verify()에서 when()와 동일하게 인수 매칭이 가능하다.

@Test
void testVerifyWithArgumentMatcher() {
    List<String> mock = mock(List.class);

    mock.add("one");
    mock.add("two");
    mock.add("three");

    verify(mock).add(startsWith("th")); // 테스트 통과
}

위에 코드에서는 add() 메서드에 th로 시작되는 값이 전달되어 실행되고 있으면 테스트가 통과한다.

메서드가 실행된 횟수 확인

verify() 두 번째 인수 Mockito#times(int)를 전달하여 메서드가 실행 된 횟수를 확인할 수 있다.

  • times()가 지정되지 않은 경우의 기본값은 times(1)을 지정하는 것과 같습니다.
@Test
void testVerifyInvocationCount() {
    List<String> mock = mock(List.class);

    mock.get(0);
    mock.get(0);
    mock.get(0);

    verify(mock, times(2)).get(0);
}

실행 결과:

list.get(0);
Wanted 2 times:
-> at com.devkuma.mockito.VerifyTest.testVerifyInvocationCount(VerifyTest.java:42)
But was 3 times:
-> at com.devkuma.mockito.VerifyTest.testVerifyInvocationCount(VerifyTest.java:38)
-> at com.devkuma.mockito.VerifyTest.testVerifyInvocationCount(VerifyTest.java:39)
-> at com.devkuma.mockito.VerifyTest.testVerifyInvocationCount(VerifyTest.java:40)

위의 예제에서 get(0)가 2번 호출되었음을 확인하였다.

  • 3번 실행하고 있으므로 에러가 발생한다.
  • 0번나 1번에 실행하여도 에러가 발생한다.

메서드가 최소 n번 실행되었는지 검증

Mockito#atLeast(int)를 사용하면 메서드가 최소 n번 실행되었는지 확인할 수 있다.

@Test
void testAtLeast() {
    List<String> mock = mock(List.class);

    mock.get(0);

    verify(mock, atLeast(2)).get(0);
}

실행 결과:

list.get(0);
Wanted *at least* 2 times:
-> at com.devkuma.mockito.VerifyTest.testAtLeast(VerifyTest.java:52)
But was 1 time:
-> at com.devkuma.mockito.VerifyTest.testAtLeast(VerifyTest.java:50)

위에 예제에서는 get(0) 최소한 2번 실행되었는지 확인한다.

  • 1번만 실행하고 있으므로 에러가 발생한다.
  • 2번 이상 실행하면 통과한다.

atLeast(1)Mockito#atLeastOnce()라는 알리아스 메서드가 제공되고 있다.

메서드가 최대 n회까지만 호출되고 있는 것을 확인

Mockito#atMost(int)를 사용하면 메서드가 많아도 n번까지만 실행되고 있는지 확인할 수 있다.

@Test
void testAtMost() {
    List<String> mock = mock(List.class);

    mock.get(0);
    mock.get(0);
    mock.get(0);

    verify(mock, atMost(2)).get(0);
}

실행 결과:

Wanted at most 2 times but was 3
org.mockito.exceptions.verification.MoreThanAllowedActualInvocations: 
Wanted at most 2 times but was 3

위에 예제에서는 get(0)이 최대 2회까지만 실행되고 있는 것을 검증하고 있다.

  • 3번 실행하고 있으므로 에러가 발생한다.
  • 0~2회 실행하면 통과한다.

atMost(1)Mockito#atMostOnce()라는 알리아스 메서드가 준비되어 있다

메서드가 한 번도 실행되지 않았는지 확인

Mockito#never()를 사용하면 메서드가 한 번도 실행되지 않았는지 확인할 수 있다.

@Test
void testNever() {
    List<String> mock = mock(List.class);

    mock.get(0);

    verify(mock, never()).get(0);
}

실행 결과:

list.get(0);
Never wanted here:
-> at com.devkuma.mockito.VerifyTest.testNever(VerifyTest.java:74)
But invoked here:
-> at com.devkuma.mockito.VerifyTest.testNever(VerifyTest.java:72) with arguments: [0]

위의 구현은 get(0)을 한 번도 실행되지 않았는지 확인한다. 한 번 실행 중이므로 오류가 발생한다.

메서드 실행 순서 확인

Mockito#inOrder(<모의 객체>)에서 InOrder를 받아와서, InOrder 메서드의 verify()을 사용하여 메서드 호출을 확인하여 메서드 호출 순서를 확인할 수 있다.

@Test
void testInvocationOrder() {
    List<String> mock = mock(List.class);

    mock.get(0);
    mock.get(2);
    mock.get(1);

    InOrder inOrder = inOrder(mock);

    inOrder.verify(mock).get(0);
    inOrder.verify(mock).get(1);
    inOrder.verify(mock).get(2);
}

실행 결과:

Verification in order failure
Wanted but not invoked:
list.get(2);
-> at com.devkuma.mockito.VerifyTest.testInvocationOrder(VerifyTest.java:91)
Wanted anywhere AFTER following interaction:
list.get(1);
-> at com.devkuma.mockito.VerifyTest.testInvocationOrder(VerifyTest.java:85)

위에 예제에서는 get(0)->get(1)->get(2)의 순서로 모의 메서드가 실행되고 있는 것을 검증하고 있다.

  • 2번째 호출이 get(2)으로 다르기 때문에 에러가 발생하였다.

여러 모의에 대한 메서드 실행 순서 확인

inOrder()인수는 가변 길이 인수이며, 여러 모의 객체를 전달할 수 있다. 반환된 InOrder을 사용하면 인수로 받은 모의 객체들에 대한 메서드의 호출 순서를 검증할 수 있다.

@Test
void testInvocationOrderWithMultipleMocks() {
    List<String> mock1 = mock(List.class);
    List<String> mock2 = mock(List.class);

    mock1.get(0);
    mock1.get(2);
    mock2.get(1);

    InOrder inOrder = inOrder(mock1, mock2);

    inOrder.verify(mock1).get(0);
    inOrder.verify(mock2).get(1);
    inOrder.verify(mock1).get(2);
}

실행 결과:

Verification in order failure
Wanted but not invoked:
list.get(2);
-> at com.devkuma.mockito.VerifyTest.testInvocationOrderWithMultipleMocks(VerifyTest.java:107)
Wanted anywhere AFTER following interaction:
list.get(1);
-> at com.devkuma.mockito.VerifyTest.testInvocationOrderWithMultipleMocks(VerifyTest.java:101)

위에 예제에서는 mock1.get(0)->mock2.get(1)->mock1.get(2)의 순서로 호출되는지 확인하였다.

  • 2번째 호출이 mock1.get(2)이었기 때문에 에러가 발생한다.

실제로 전달된 인수를 얻는다.

ArgumentCaptor를 사용하면 모의 메서드에 전달된 실제 인수를 (캡처하여) 받아올 수 있다.

@Test
void testCaptor() {
    List<String> mock = mock(List.class);

    mock.get(0);
    mock.get(1);
    mock.get(9);

    ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);

    verify(mock, times(3)).get(captor.capture());

    System.out.println("captor.getValue()     = " + captor.getValue());
    System.out.println("captor.getAllValues() = " + captor.getAllValues());
}

실행 결과:

captor.getValue()     = 9
captor.getAllValues() = [0, 1, 9]
  • ArgumentCaptor#forClass(Class)으로 캡처하려는 인수의 ArgumentCaptor을 생성한다.

    • Class는 캡처하려는 인수의 유형에 맞춰야 한다.
  • 생성한 ArgumentCaptorcapture() 메서드를 verify()에 의한 검증 통해서 캡처하고 싶은 인수에서 실행한다.

  • 캡처 한 결과는 getValue() 또는 getAllValues()으로 받아올 수 있다.

    • getValue() 마지막으로 캡처한 값을 반환한다.
    • getAllValues() 메서드가 여러 번 호출되면, 각 호출에서 전달된 인수의 값을 목록에서 얻을 수 있다.
      • 메서드가 호출된 순서대로 목록에 설정된다.

Spy - 스파이

spy(T) 메서드에 임의의 객체를 전달하여 스파이를 만들 수 있다.

package com.devkuma.mockito;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

public class SpyTest {

    @Test
    void test() {
        Hoge hoge = new Hoge();
        Hoge spy = spy(hoge);

        when(spy.size()).thenReturn(100);

        spy.add("hello");
        spy.add("world");
        spy.setName("Spied!");

        System.out.println("spy       = " + spy);
        System.out.println("spy.size  = " + spy.size());
        System.out.println("hoge      = " + hoge);
        System.out.println("hoge.size = " + hoge.size());
    }

    public static class Hoge implements Cloneable {
        private List<String> list = new ArrayList<>();
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public void add(String value) {
            list.add(value);
        }

        public int size() {
            return list.size();
        }

        @Override
        public String toString() {
            return "Hoge{" + "list=" + list + ", name='" + name + '\'' + '}';
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
            throw new CloneNotSupportedException();
        }
    }
}

실행 결과:

spy       = Hoge{list=[hello, world], name='Spied!'}
spy.size  = 100
hoge      = Hoge{list=[hello, world], name='null'}
hoge.size = 2
  • 스파이 메서드는 스텁되지 않은 경우에 실제 메서드 처리를 수행한다.

    • 위에 예제에서는 스텁화하는 것을 size() 제외하고는 실제 메서드가 실행된다.
    • 일부 메서드만 스텁화하고 싶을 때 사용할 수 있다.
  • 스파이는 원본 객체의 복사본을 기반으로 만들어 진다.

    • 스파이 자체의 상태를 변경해도 원래 객체의 상태는 변경되지 않는다.
      • 위에 예제에서는 name 상태는 별도이다.
    • 다만 Deep copy가 아니기 때문에, 다른 객체에 참조를 되고 있다면 공유된다.
      • 위에 예제에서는 list 상태가 공유된다.
    • 복사에 clone()는 사용되지 않는다.
      • 혹시 clone()으로 복사가 실행되면, 위에 예제에서는 에러가 발생한다.

실제 메서드를 호출하지 않고 Stub으로 만들기

먼저 실패하는 예제를 보도록 하겠다. 아래 예제에서는 List를 스파이로 만들고 get(0) 결과를 스텁으로 만들려고 한다.

@Test
void testStubbingSpyWithError() {
    List<String> list = new ArrayList<>();
    List<String> spy = spy(list);

    when(spy.get(0)).thenReturn("spied");

    System.out.println("spy.get(0) = " + spy.get(0));
}

실행 결과:

Index 0 out of bounds for length 0
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0

위에 예제는 IndexOutOfBoundsException가 발생한다.

  • when(spy.get(0))의 시점에서 아직 get(0)는 스텁으로 만들어지지 않았다.
  • 그래서 get(0)는 실제의 메서드가 실행되어서 IndexOutOfBoundsException가 발생하게 되어 버린다.

위에 방법으로는 스파이 메서드를 스텁으로 만드려고 하면, when(<메서드 호출>)만으로는 실제 메서드가 실행되는 불편을 겪을 수 있다.
이 문제는 다음과 같이 doReturn()을 사용하여 피할 수 있다.

@Test
void testStubbingSpyNoError() {
    List<String> list = new ArrayList<>();
    List<String> spy = spy(list);

    doReturn("spied").when(spy).get(0);

    System.out.println("spy.get(0) = " + spy.get(0));
}

실행 결과:

spy.get(0) = spied
  • doReturn(<반환값>)으로 시작하고, 체인으로 이어서 when(<스파이 객체>)을 선언한다.

  • when()의 반환값의 형태는 인수로 전달한 스파이 객체의 형태가 되어 있으므로, 그대로 스텁화하고 싶은 메서드를 호출한다.

    • ArgumtnetMatchers를 사용하여 인수의 일치 조건을 지정할 수도 있다.
  • 이렇게 하면 스파이 객체 자체의 메서드를 실행하지 않고, 스텁을 정의 할 수 있으므로 이전 문제를 피할 수 있다.

  • 스파이 메서드를 실행하지 않고, 예외가 발생하도록 스텁으로 지정하고 싶은 경우는 doThrow()를 사용한다.

추상 클래스 스파이

일반적으로 스파이를 생성하려면 생성된 인스턴스가 필요하지만, Class 객체를 매개 변수로 하는 spy(Class)을 사용하면 생성된 인스턴스 없이 스파이를 생성 할 수 있다.

package com.devkuma.mockito;

import org.junit.jupiter.api.Test;

import static org.mockito.Mockito.spy;

public class AbstractSpyTest {

    @Test
    void test() {
        AbstractClass mock = spy(AbstractClass.class);
        System.out.println("mock.concreteMethod()       = " + mock.concreteMethod());
        System.out.println("mock.abstractStringMethod() = " + mock.abstractStringMethod());
    }

    public static abstract class AbstractClass {

        public String concreteMethod() {
            return "concreteMethod";
        }

        abstract public String abstractStringMethod();
    }
}

실행 결과:

mock.concreteMethod()       = concreteMethod
mock.abstractStringMethod() = null
  • 추상 클래스와 같이 인스턴스 생성이 어려운 클래스에 대한 스파이를 생성할 때 사용할 수 있다.
    • 추상 메서드의 반환값은 모의 객체의 기본 반환값과 같은 값을 받는다.
  • 스파이 대상 클래스에는 기본 생성자가 필요하다.

다른 객체에 위임하는 형태로 모의(스파이) 객체 만들기

mock(Class, AdditionalAnswers.delegatesTo(<위임하는 객체>))하는 것으로, 각 메서드의 처리를 위양처 오브젝트에 위양하는 모의를 작성할 수 있다.

package com.devkuma.mockito;

import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;

public class DelegateTest {
    
    @Test
    void test() {
        HogeFunction mock = mock(HogeFunction.class, delegatesTo(new FinalClass()));

        when(mock.world()).thenReturn("mocked");

        System.out.println(mock.hello(10));
        System.out.println(mock.world());
    }

    public interface HogeFunction {
        String hello(int n);

        String world();
    }

    public final static class FinalClass {
        public String hello() {
            return "hello()";
        }

        public String hello(int n) {
            return "hello(" + n + ")";
        }

        public String world() {
            return "world()";
        }
    }
}

실행 결과:

hello(10)
mocked
  • 위임 대상 객체가 모의 형태로 상속 받을 필요는 없다.
    • 서명(signature)이 일치하는 메서드에 위임된다.
    • 위임되는 메서드가 없는 경우는 런타임 에러가 발생한다.
  • 이는 다음과 같은 객체나 메서드를 모의/스텁화하고 싶은 경우에 활용된다.
    • final 클래스와 메서드
      • mockito 는 표준적으로 final 클래스/메서드를 모의/스텁화할 수 없다. 하려면 뒤에서 설명할 inline mock maker라는 확장 기능을 사용하면 가능하다.
    • 이미 프록시된 객체
      • Spring의 Bean을 말하나?

모의 Serializable 가능

mock() 메서드의 두번째 인수에 Mockito.withSettings().serializable()을 전달하면, 생성된 모의는 직렬화 할 수 있다.

package com.devkuma.mockito;

import org.junit.jupiter.api.Test;

import java.io.Serializable;
import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;

public class SerializableTest {

    @Test
    void test() {
        List<String> noSerializableMock = mock(List.class);
        System.out.println(noSerializableMock instanceof Serializable);

        List<String> serializableMock = mock(List.class, withSettings().serializable());
        System.out.println(serializableMock instanceof Serializable);
    }
}

실행 결과:

false
true

One liner로 모의를 생성하여 스텁을 정의

thenReturn()/thenThrow() 호출 후에 getMock()으로 호출하여, 모의 객체를 얻을 수 있기에, 모의 생성에서 스텁 정의까지 하나의 라인으로 작성할 수 있다.

package com.devkuma.mockito;

import org.junit.jupiter.api.Test;

import java.util.List;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class OneLinerTest {
    List<String> mock = when(mock(List.class).get(0)).thenReturn("mocked").getMock();

    @Test
    void test() {
        System.out.println("mock.get(0) = " + mock.get(0));
    }
}

실행 결과:

mock.get(0) = mocked

어노테이션으로 정의하기

MockitoAnnotations#openMocks(<테스트 객체 인스턴스>)을 실행하면, 어노테이션을 사용하여 모의 또는 스파이를 정의 할 수 있다.

package com.devkuma.mockito;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;

import java.util.ArrayList;
import java.util.List;

import static org.mockito.Mockito.*;

public class AnnotationTest {

    @Mock
    List<String> mock;
    @Spy
    List<String> spy = new ArrayList<>();
    @Captor
    ArgumentCaptor<String> captor;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void test() {
        when(mock.get(0)).thenReturn("mocked");
        System.out.println("mock.get(0) = " + mock.get(0));

        spy.add("hello");
        spy.add("world");
        doReturn("spied").when(spy).get(0);
        System.out.println("spy.get(0) = " + spy.get(0));
        System.out.println("spy.get(1) = " + spy.get(1));

        verify(spy, atLeastOnce()).add(captor.capture());
        System.out.println("captor.getAllValues() = " + captor.getAllValues());
    }
}

실행 결과:

mock.get(0) = mocked
spy.get(0) = spied
spy.get(1) = world
captor.getAllValues() = [hello, world]
  • 테스트 클래스에 선언한 필드에 어노테이션을 붙여서 정의할 수 있다.
    • @Mock으로 모의를 정의 할 수 있다.
    • @Spy으로 스파이를 정의할 수 있다.
      • 필드에 객체가 설정되어 있으면, 그 객체를 바탕으로 스파이가 만들어 진다.
      • 필드에 아무것도 설정되어 있지 않으면, 디폴트 생성자로 생성된 객체가 사용된다.
        • spy(Class)를 사용했을 때와 같은 동작을 한다.
    • @Captor에서 ArgumentCaptor을 정의할 수 있다.

JUnit 5용 확장

mockit-core 대신에 org.mockito:mockito-junit-jupiter를 의존성를 설정한다.
build.gradle

dependencies {
    // testImplementation "org.mockito:mockito-core:4.8.0"
    testImplementation "org.mockito:mockito-junit-jupiter:4.8.0"
    testImplementation "org.junit.jupiter:junit-jupiter:5.8.2"
}
  • mockito-junit-jupiter을 의존성에 추가하면 MockitoExtension 이라는 JUnit5 확장 클래스를 사용할 수 있다.
  • MockitoExtension를 사용하면, MockitoAnnotations#openMocks(Object)을 자동으로 할 수 있다.
  • 또한, 메서드 인수로 모의를 받을 수도 있다.
package com.devkuma.mockito;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.Map;

import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class JUnit5ExtensionTest {
    @Mock
    List<String> mockList;

    @Test
    void test() {
        when(mockList.get(0)).thenReturn("mocked");
        System.out.println("mockList.get(0) = " + mockList.get(0));
    }

    @Test
    void testMethodArgument(@Mock Map<String, String> mockMap) {
        when(mockMap.get("hello")).thenReturn("mocked");
        System.out.println("mockMap.get(hello) = " + mockMap.get("hello"));
    }
}

실행 결과

mockList.get(0) = mocked
mockMap.get(hello) = mocked

inline mock maker

inline mock maker 이라는 확장을 도입하면, 일반적인 mockito 에서는 실현할 수 없는 final 클래스의 Mocking으로 하려 static 메서드의 스텁화등을 할 수 있게 된다.

inline mock maker는 org.mockito:mockit-inline의 의존관계를 설정하는 것만으로 사용할 수 있게 된다.

build.gradle

dependencies {
//    testImplementation "org.mockito:mockito-core:4.8.0"
    testImplementation "org.mockito:mockito-inline:4.8.0"
    testImplementation "org.junit.jupiter:junit-jupiter:5.8.2"
}

final 클래스와 메서드를 모의/스텁으로 만들기

위에 같이 inline mock maker를 설정한 것만으로 final 클래스나 메서드를 모의/스텁으로 만들 수 있게 된다.

package sandbox.mockito;

import org.junit.jupiter.api.Test;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MockFinalClassTest {

    @Test
    void test() {
        FinalClass mock = mock(FinalClass.class);

        when(mock.hello()).thenReturn("mocked");

        System.out.println("mock.hello() = " + mock.hello());
    }

    public static final class FinalClass {
        public String hello() {
            return "world";
        }
    }
}

실행 결과:

mock.hello() = mocked

static 클래스와 메서드를 모의/스텁으로 만들기

inline mock maker를 설정하고 Mockito#mockStatic(Class)으로 스텁으로 만들고 싶은 static 메서드가 있는 클래스을 지정하면, static 클래스나 메서드를 모의/스텁으로 만들 수 있게 된다.

package com.devkuma.mockito;

import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

public class StaticMethodTest {

    @Test
    void test() {
        System.out.println("Hoge.hello() = " + Hoge.hello("before"));

        try (MockedStatic<Hoge> mocked = Mockito.mockStatic(Hoge.class)) {
            mocked.when(() -> Hoge.hello("test")).thenReturn("mocked");

            System.out.println("Hoge.hello() = " + Hoge.hello("test"));

            mocked.verify(() -> Hoge.hello("test"));
        }

        System.out.println("Hoge.hello() = " + Hoge.hello("after"));
    }

    public static class Hoge {
        public static String hello(String arg) {
            return "world (arg=" + arg + ")";
        }
    }
}

실행 결과

Hoge.hello() = world (arg=before)
Hoge.hello() = mocked
Hoge.hello() = world (arg=after)
  • static 메소드의 스텁이 유효한 것은 mockStatic()의 반환값인 MockedStaticclose()가 될 때까지만 이다.
    • try-with-resources 문으로 close() 되도록 하는 것을 추천한다.
  • static 메소드의 스텁은 MockedStaticwhen(Verification) 메소드로 정의한다.
    • 인수에는 스텁화하고 싶은 static 메소드의 호출을 구현한 Verification를 건네준다.
    • Verification은 함수형 인터페이스이므로 람다 식으로 스텁으로 만들고 싶은 메소드를 호출하도록 사용하는 것이 편하다.
      • 인수가 없는 메소드라면, 메소드 참조로 작성하는 것도 가능 (mocked.when(SomeClass::staticMethod))
    • 반환값이 void의 메소드도 마찬가지로 when()으로 스텁로 만들 수 있다.
    • mocked.doReturn(...).when(...)와 같이 작성할 수는 없다.
  • verify()Mocked에 준비되어 있는 메소드를 사용해 검증한다.

생성자에서 모의를 반환하게 한다.

Mockito#mockConstrcution(Class)으로 Class 객체를 전달한 클래스의 생성자는 모의 객체를 반환한다.

package com.devkuma.mockito;

import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;

import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.when;

public class MockConstructorTest {

    @Test
    void test() {
        System.out.println("new Hoge().hello() = " + new Hoge().hello());

        try (MockedConstruction<Hoge> mocked = mockConstruction(Hoge.class)) {
            Hoge mock = new Hoge();

            when(mock.hello()).thenReturn("mocked");

            System.out.println("mock.hello() = " + mock.hello());
        }

        System.out.println("new Hoge().hello() = " + new Hoge().hello());
    }

    public static class Hoge {
        public String hello() {
            return "world";
        }
    }
}

실행 결과:

new Hoge().hello() = world
mock.hello() = mocked
new Hoge().hello() = world
  • 생성자에서 모의를 반환하는 것을 mockConstrcution()의 반환값으로 있는 MockedConstruction이지만 이는 close() 되기까지만 이다.
    • try-with-resources 문으로 close() 되도록 하는 것을 추천한다.
  • 스텁 및 검증은 일반적인 Mockito을 사용 방법과 동일하게 when().verify()을 사용한다.

모의할 수 없는 클래스 정의

아래 DoNotMock라는 어노테이션으로 모의를 할 수 없는 클래스를 정의해 보도록 하겠다.

package org.mockito;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Target({TYPE})
@Retention(RUNTIME)
@Documented
public @interface DoNotMock {
    
    String reason() default "Create a real instance instead.";
}
  • @TargetTYPE으로 되어 있고, @Retention으로 RUNTIME이 지정되어 있다.

Hoge 클래스에 @DoNotMock 어노테이션을 설정하고 모의를 실행 본다.

package com.devkuma.mockito;

import org.junit.jupiter.api.Test;
import org.mockito.DoNotMock;

import static org.mockito.Mockito.mock;

public class DoNotMockTest {

    @Test
    void test() {
        mock(Hoge.class);
    }

    @DoNotMock
    public static class Hoge {
    }
}

실행 결과:

class com.devkuma.mockito.DoNotMockTest$Hoge is annotated with @org.mockito.DoNotMock and can't be mocked. Create a real instance instead.
org.mockito.exceptions.misusing.DoNotMockException: class com.devkuma.mockito.DoNotMockTest$Hoge is annotated with @org.mockito.DoNotMock and can't be mocked. Create a real instance instead.
  • 실행해 보면 모의가 실패한다.
  • org.mockito.DoNotMock라는 어노테이션을 만들어서 클래스로 설정하면, 클래스의 모의를 금지 할 수 있다.

참조




최종 수정 : 2024-04-14