Test Double과 Mockito 기초 사용법 알아보기

MODUGGAGI·2026년 3월 23일

Test

목록 보기
2/4

Test Double이란??

Test Double은 테스트를 위해 사용하는 대역을 의미한다.

Double은 영화나 드라마에서 주연 대신 연기하는 대역이라는 의미라고 한다.

그래서 Test Double이라고 하면 Test 코드에서 실제 객체 대신 사용하는 가짜 객체, 즉 대역인 것이다.

이러한 Test Double의 종류에는 아래와 같이 나눌 수 있다.


대역(Test Double)

1. 더미(Dummy)

아무 동작도 수행하지 않는 객체로, 메서드 호출을 위해 단순히 자리를 채우기 위해 사용된다.

2. 스텁(Stub)

구현을 단순한 것으로 대체한다.
테스트에 맞게 단순히 원하는 동작을 수행한다.

3. 가짜(Fake)

제품에는 적합하지 않지만, 실제 동작하는 구현을 제공한다.

4. 스파이(Spy)

호출된 내역을 기록한다.
기록한 내용은 테스트 결과를 검증할 때 사용한다.
스텁이기도 하다.

5. 모의(Mock)

기대한 대로 상호작용하는지 행위를 검증한다.
기대한 대로 동작하지 않으면 예외를 발생시킬 수 있다.
모의 객체는 스텁이자 스파이도 될 수 있다.


Mockito

Mockito 라이브러리는 개발자가 편리하게 Mock 객체를 생성하고, 생성된 Mock 객체를 통해 메서드의 동작을 정의하거나, 호출 여부를 검증하는 등의 방법으로 Stub, Spy, Mock의 역할을 표현할 수 있도록 해준다.

Dummy의 경우 메서드 호출을 위해서 어쩔 수 없이 필요하지만 아무 기능을 수행할 필요가 없는 객체가 필요할 때 사용하는데, Mockito를 통하지 않더라도 단순히 new Object()를 통해서도 구현이 가능하기에 여기선 설명하지 않는다.

Fake의 경우는 보통 Mockito를 사용하지 않고 직접 테스트용 구현체를 생성해서 사용하게 된다.


왜 하필 Mock이지??

Mockito 라이브러리가 많은 Test Double중 하필 Mock 객체를 사용하는 이유는 뭘까??

그 이유는 위의 정의에서 알아볼 수 있다.

Mock이 다른 Test Double의 역할을 모두 표현할 수 있는 가장 일반적인 형태이기 때문이다.

Mock을 사용하면 특정 메서드의 호출을 가로채고 기록할 수 있다.

이것이 가능한 이유는 Mock 객체가 프록시이기 때문!!

프록시 객체로 Mock을 생성하기 때문에 실제 객체로의 요청을 가로챌 수 있게 된다.
‼️ 프록시 객체 생성 방식은 글의 마지막에서 간단히 다룬다

Mockito는 Mock의 이 기능을 활용해서

  1. 메서드의 동작을 정의 → Stub
  2. 메서드 호출 여부를 검증 → Mock (행위 검증)
  3. 실제 객체를 감싸 호출 내역을 기록 → Spy

와 같이 사용할 수 있는 API를 제공해 다양한 Test Double의 역할을 사용할 수 있게 해준다.

따라서 Mockito는 Mock객체를 생성하는 것을 시작으로 라이브러리의 기능들을 사용할 수 있게 되어있는 것이다.


Mockito 라이브러리 메서드 알아보기

Mockito 공식 문서 링크

public class Blackjack {

	public boolean start() {
		// 테스트용 빈 메서드
	}
}
class BlackjackTest {

    private Blackjack blackjack = mock(Blackjack.class);
    
    @Test
    void mock_test() {
		when(blackjack.start()).thenReturn(true);

		boolean result = blackjack.start();

		verify(blackjack).start();
		assertThat(result).isTrue();
    }
}

아래에서 설명할 메서드들은 위의 코드를 기반으로 한다.

설명을 단순화하기 위해 같은 start() 메서드에 다양한 타입의 인자가 전달되기도 하고, 반환 타입도 다양해지기도 하지만 이는 이해를 위해 단순화한 것이다!!

실제로는 각각 서로 다른 시그니처를 가진 별개의 상황이라고 봐야 한다!!


1. Mock 객체 생성하기

Mockito를 사용하려면 앞서 설명한 것 처럼 당연하게도 Mock 객체를 생성해야 한다.

아래의 2가지 방법으로 Mock 객체 생성이 가능하다.

  • mock()
    private Blackjack blackjack = mock(Blackjack.class);
  • @Mock
    @ExtendWith(MockitoExtension.class)
    class BlackjackTest {
    
        @Mock
        private Blackjack blackjack;
    }
    • @Mock 어노테이션을 사용하면 Mockito가 테스트 실행 시점에 필드를 초기화해서 Mock 객체를 넣어준다.
    • JUnit5 기준으로 테스트 클래스수준에 @ExtendWith(MockitoExtension.class) 를 붙여주어야 한다.
  • Allows shorthand mock creation.
  • Minimizes repetitive mock creation code.
  • Makes the test class more readable.
  • Makes the verification error easier to read because the field name is used to identify the mock.
  • Automatically detects static mocks of type MockedStatic and infers the static mock type of the type parameter.

공식 문서에 따르면 @Mock 어노테이션을 사용하면 짧고 읽기 좋게 Mock 객체를 생성할 수 있다고 하며, 반복적인 생성 코드를 줄이고, 테스트 클래스를 더 읽기 좋게 만든다고 한다.


2. Stub 구성하기

Stub은 특정 메서드가 호출되면 어떤 값을 반환할지 미리 정하는 것이다.

Mockito에서는 Stub을 편리하게 정의할 수 있게 API를 제공한다.

  • 원하는 값을 반환하게 하고 싶을 때: when(...).thenReturn(...)
    when(blackjack.start()).thenReturn(true);
    blackjack클래스의 start() 메서드가 호출되면 truereturn 해라 라는 의미.
    이때 thenReturn()의 인자로 전달되는 값은 when에 들어가는 메서드의 반환값과 동일해야 한다.
    예를 들어 start() 메서드의 반환값이 String이라면 다음과 같이 작성하는 것이 가능하다
    when(blackjack.start()).thenReturn("start");
    반환값이 없는 void의 경우에는 when(…).thenReturn(…)을 아예 사용하지 못한다.

  • 예외를 발생시키고 싶을 때: when(...).thenThrow(...)
    when(blackjack.start()).thenThrow(new IllegalStateException());
  • 반환 타입이 없을 때는 아래에서 설명할 verify()를 사용해서 해당 메서드의 호출 유무를 검증하면 된다!

2-1. BDDMockito 사용해서 Stub 구성하기

여기서 BDD란 Behavior-Driven Development를 의미한다.

행위-주도 개발로 테스트 코드를 작성할 때 사용한

given-when-then 사이클과 같이 비슷한 흐름으로 작성하고 싶을 때 사용하면 된다.

public class BDDMockito extends Mockito {}

BDDMockito는 Mockito를 상속받은 클래스이기 때문에 기능들을 그대로 사용할 수 있다.

  • 원하는 값을 반환하게 하고 싶을 때: given(...).willReturn(...)
    given(blackjack.start()).willReturn(true);
    • Mockito의 when(...).thenReturn(...) 과 동일하게 사용할 수 있다.

  • 예외를 발생시키고 싶을 때: given(...).willThrow(...)
    given(blackjack.start()).willThrow(new IllegalStateException());
    • Mockito의 when(...).thenThrow(...) 과 동일하게 사용할 수 있다.

3. 메서드 호출 여부 검증하기

Mock 객체의 역할 중 하나는 실제로 해당 Mock 객체가 불렸는지 검증을 하는 것이다.

Mockito에서는 verify(...)를 사용해 어떤 메서드가 호출되었는지 검증할 수 있다.

BDDMockito에서는 then(...).should() 를 사용한다.

  • verify(...) , then(...).should()
    //Mockito
    verify(blackjack).start();
    
    //BDDMockito
    then(blackjack).should().start();
    • blackjack 객체의 start() 메서드가 실제로 “한 번” 호출되었는지 확인한다.

인자로 객체만 넣게 되면 해당 객체의 메서드가 1번만 호출되었는지 확인하는 용도이다.

아래의 메서드들은 메서드의 호출횟수를 검증하기 위해 사용할 수 있는 메서드이다.

아래에 나오는 코드는 모두 독립적인 예시이다.

설명을 단순화하기 위해 같은 start() 메서드에 다양한 타입의 인자가 전달되는 것처럼 표현했지만,
실제로는 각각 서로 다른 시그니처를 가진 별개의 상황이라고 봐야 한다!!

  • only()
    //Mockito
    verify(blackjack, only()).start();
    
    //BDDMockito
    then(blackjack).should(only()).start();
    • blackjack.start()가 유일하게 호출되었는지 검증 blackjack 객체의 다른 메서드들은 호출되지 않고 오로지 start() 메서드만 호출되었는지를 검증한다.
  • times(int)
    //Mockito
    verify(blackjack, times(3)).start();
    
    //BDDMockito
    then(blackjack).should(times(3)).start();
    • 지정된 횟수 만큼 호출되었는지 검증
  • never()
    //Mockito
    verify(blackjack, never()).start();
    
    //BDDMockito
    then(blackjack).should(never()).start();
    • 호출되지 않았는지를 검증
  • atLeast(int)
    //Mockito
    verify(blackjack, atLeast(3)).start();
    
    //BDDMockito
    then(blackjack).should(atLeast(3)).start();
    • 적어도 지정된 횟수 만큼 호출되었는지 검증
  • atLeastOnce()
    //Mockito
    verify(blackjack, atLeastOnce()).start();
    
    //BDDMockito
    then(blackjack).should(atLeastOnce()).start();
    • atLeast(1)과 동일
  • atMost(int)
    //Mockito
    verify(blackjack, atMost(3)).start();
    
    //BDDMockito
    then(blackjack).should(atMost(3)).start();
    • 최대 지정된 횟수 만큼 호출되었는지 검증

4. 인자 조건 다루기

Stub을 구성하거나, 호출을 검증할 때 전달해야할 인자가 있는 경우도 있다.

이때 사용할 수 있는 것이 ArgumentMatchers클래스의 메서드들이다.

  • anyInt(), anyShort(), anyLong(), anyByte(), anyChar(), anyDouble(), anyFloat(), anyBoolean()
    • 뒤에 주어진 기본 데이터 타입에 대해서 인자로 전달된 임의의 값이 타입만 일치하면 전부 OK 처리한다.
    • 특정 값 자체는 중요하지 않고 원하는 타입의 값이 전달되었는지만 보고 싶을 때 사용할 수 있다
  • anyString()
    • 인자로 전달된 값이 문자열이면 전부 OK 처리한다.
  • any()
    • 인자로 전달된 모든 값에 대해서 타입만 맞으면 OK 처리한다.
    • 대신 null도 OK 처리할 수 있기 때문에 null 값을 막고 싶을 경우 인자로 특정한 클래스를 지정해주면 된다.
  • anyList(), anySet(), anyMap(), anyCollection()
    • 인자로 전달된 각 컬렉션들에 대해서 타입이 맞으면 OK 처리한다.
  • matches(String), matches(Pattern)
    • 정규표현식을 사용해서 인자로 전달된 값의 일치 여부를 확인해서 맞을 경우 OK 처리한다.
  • eq(…)
    • 인자로 전달된 값이 지정한 값과 일치하는 지 확인 후 맞을 경우 OK 처리한다.

아래의 예시와 같이 사용할 수 있다.

// Mockito - Stub 구성
when(blackjack.start(anyInt())).thenReturn(true);

// Mockito - 호출 검증
verify(blackjack).start(eq("dealer"));
verify(blackjack).start(matches("start-[0-9]+"));

// BDDMockito - Stub 구성
given(blackjack.start(any(TestParameter.class))).willReturn(true);

// BDDMockito - 호출 검증
then(blackjack).should().start(anyList());

위 코드는 모두 독립적인 예시이다.

설명을 단순화하기 위해 같은 start() 메서드에 다양한 타입의 인자가 전달되는 것처럼 표현했지만,
실제로는 각각 서로 다른 시그니처를 가진 별개의 상황이라고 봐야 한다!!

4-1. 전달되는 인자의 개수가 여러개일 경우

전달되는 인자의 개수가 여러개일 경우 특정 인자에만 ArgumentMatcher클래스의 메서드를 사용하는 것은 불가능하다

given(blackjack.start(any(), "test")).willReturn(true);
  • 이와 같이 첫번째 인자에는 any()로 일치하는 모든 타입에 대해 가능하게 설정하고 2번째 인자는 단순 String값을 넣어주는 것은 허용되지 않는다.
given(blackjack.start(any(), eq("test"))).willReturn(true);
  • 이렇게 2번째 인자도 ArgumentMatcher클래스의 eq() 메서드를 사용해서 정확하게 일치하는지 여부를 판단해야 한다.

결론은 인자에 1개라도 ArgumentMatcher 클래스를 사용하게 되면 모든 인자에 적용하거나, 아니면 1개도 사용하지 않거나 식으로 일관되게 해주어야 한다.


5. 인자 직접 검증하기 (인자 캡쳐)

Mock 객체의 메서드가 호출될 때 실제로 어떤 인자가 전달되었는지 직접 검증하고 싶다면 Mockito의 ArgumentCaptor를 사용하면 된다.

앞에서 살펴본 eq(...), any...() 와 같은 인자 매처들은 전달된 값이 특정 조건을 만족하는지만 확인할 수 있다.

반면 ArgumentCaptor는 실제로 전달된 인자를 꺼내와서 그 내부 값까지 직접 검증할 수 있다는 차이가 있다.

String이나 int와 같은 타입은 쉽게 검증할 수 있지만 인자로 객체가 전달되는 경우는 쉽게 검증하기 어려운데, 이때 ArgumentCaptor를 사용하면 된다.

  • 인자가 특정 조건을 만족하는지만 보면 될 때 → ArgumentMatchers
  • 실제로 어떤 객체가 전달되었는지 꺼내서 보고 싶을 때 → ArgumentCaptor

와 같이 사용할 수 있다.

public class Blackjack {

		public void start(TestParameter parameter) {
				// 테스트
		}
}
@ExtendWith(MockitoExtension.class)
class BlackjackTest {

    @Mock
    private Blackjack blackjack;

    @DisplayName("start() 호출 시 전달된 객체 내부 값을 검증할 수 있다")
    @Test
    void blackjack_start_test() {
        blackjack.start(new TestParameter("dealer", 3));
        
        ArgumentCaptor<TestParameter> captor = ArgumentCaptor.forClass(TestParameter.class);

        // Mockito
				verify(blackjack).start(captor.capture());
				// BDDMockito
				then(blackjack).should().start(captor.capture());

        TestParameter capturedParameter = captor.getValue();

        assertThat(capturedParameter.getDealerName()).isEqualTo("dealer");
        assertThat(capturedParameter.getPlayerCount()).isEqualTo(3);
    }
}

예시 코드에서는 2가지 경우를 한번에 보여주기 위해서 Mockito와 BDDMockito를 한개의 테스트 코드안에 적었다.

ArgumentCaptor를 통해 캡쳐한 객체 내부의 값을 가져오기 위해서는 getValue()를 하면 된다.

  • getValue()
    • 마지막으로 캡쳐된 인자 1개를 반환한다.
    • 메서드가 한 번만 호출된 경우 사용하기 좋다.
  • getAllValues()
    • 캡쳐된 모든 인자를 List로 반환한다.
    • 메서드가 여러 번 호출된 경우 사용하기 좋다.

ArgumentCaptor는 크게 다음의 3 경우에 유용하게 사용할 수 있다!

  1. 메서드에 전달된 객체 내부 값을 확인하고 싶을 때
  2. 메서드 안에서 새 객체를 만들어 넘길 때
  3. eq()로는 표현하기 애매할 때

5-1. @Captor 사용하기

@Mock과 마찬가지로 ArgumentCaptor@Captor를 통해서 간편하게 ArgumentCaptor를 선언할 수 있다.

@ExtendWith(MockitoExtension.class)
class BlackjackTest {

    @Mock
    private Blackjack blackjack;

    @Captor
    private ArgumentCaptor<TestParameter> captor;

    @DisplayName("start() 호출 시 전달된 객체 내부 값을 검증할 수 있다")
    @Test
    void blackjack_start_test() {
        blackjack.start(new TestParameter("dealer", 3));

        verify(blackjack).start(captor.capture());

        TestParameter capturedParameter = captor.getValue();

        assertThat(capturedParameter.getDealerName()).isEqualTo("dealer");
        assertThat(capturedParameter.getPlayerCount()).isEqualTo(3);
    }
}

6. Spy 사용하기

Spy는 실제 객체를 기반으로 만들어지는 Test Double이다.

앞에서 사용한 Mock 객체는 모든 동작을 Mockito가 가로채는 완전한 가짜 객체였다면,
Spy는 실제 객체를 감싸고 있기 때문에 실제 객체의 메서드가 그대로 호출된다.

  • Mock → 완전한 가짜 객체 → Mock 객체의 메서드 호출
  • Spy → 실제 객체를 기반으로 동작하는 객체 → 실제 객체의 메서드 호출

따라서 실제 동작은 유지하면서 호출 여부까지 검증하고 싶을 때 Spy를 사용한다

6-1. spy()

Blackjack spyBlackjack = spy(new Blackjack());

boolean result = spyBlackjack.start();

verify(spyBlackjack).start();
assertThat(result).isTrue();
  • spy()를 사용하면 실제 객체를 기반으로 하는 Spy 객체를 생성할 수 있다.
  • 이 경우 start()를 호출하면 실제 Blackjack 객체의 start() 메서드가 실행된다.
  • 동시에 verify(...)를 사용해서 해당 메서드가 호출되었는지도 검증할 수 있다.

6-2. @Spy

@ExtendWith(MockitoExtension.class)
class BlackjackTest {

    @Spy
    private Blackjack blackjack = new Blackjack();
}
  • @Spy를 사용하면 spy(...)를 직접 호출하지 않고도 Spy 객체를 선언할 수 있다.
  • @Mock과 마찬가지로 JUnit5 기준 테스트 클래스에 @ExtendWith(MockitoExtension.class)를 붙여 사용하면 된다.

6-3. Spy에서 Stub을 구성할 때 주의할 점

Spy는 실제 메서드를 호출하기 때문에,

Mock 객체에서 사용했던 when(...).thenReturn(...)을 그대로 사용하면 Stub을 구성하는 시점에 실제 객체의 메서드가 실행될 수 있다.

따라서 doReturn()을 사용해야 한다.

공식문서의 내용을 보면 다음과 같다.

Mockito 공식 문서의 spy 관련 부분

Important gotcha on spying real objects!

  1. Sometimes it's impossible or impractical to use when(Object) for stubbing spies. Therefore when using spies please consider doReturn|Answer|Throw() family of methods for stubbing.
List list = new LinkedList();
List spy = spy(list);

//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
when(spy.get(0)).thenReturn("foo");

//You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);

스파이 객체의 경우 when을 사용하는 것이 불가능하거나 실용적이지 못할 때가 있다고 한다.

공식 문서 예시의 경우 when() 내부에서 spy.get(0)을 호출하게 된다.

그런데 spy 객체는 현재 0번 인덱스가 존재하지 않기 때문에 IndexOutOfBoundsException 이 발생한다.

따라서 객체의 메서드를 호출하지 않는 doReturn() 계열 메서드를 사용하는 것을 권장한다.

Mock 객체의 경우 Mockito 가 만든 Mock 객체가 실제 객체로의 요청이 가기전에 요청을 가로채고 값을 반환하기 때문에 아무 문제가 되지 않았던 것!!

Blackjack spyBlackjack = spy(new Blackjack());

// ❌ 에러 가능성!!
when(spyBlackjack.start()).thenReturn(false);

// ✅ 안전하게
doReturn(false).when(spyBlackjack).start();

assertThat(spyBlackjack.start()).isFalse();
  • Spy에서 doReturn(...)을 사용하면 실제 메서드를 먼저 실행하지 않고 반환값을 지정할 수 있다.

부록

프록시 생성 방식은

  1. 인터페이스가 있는 경우
  2. 인터페이스가 없이 구체 클래스만 있는 경우

로 나눌 수 있다.

1번의 경우 JDK 동적 프록시를 통해 인터페이스를 구현한 프록시 객체를 생성하고,

2번의 경우 바이트코드를 조작해서 동적으로 구체 클래스를 상속받는 클래스를 생성해서 프록시 객체를 생성한다.

Spring에서는 1,2 번 방식을 모두 사용하지만 Spring Boot에서는 기본 설정값으로 항상 CGLIB를 사용하여 바이트코드를 조작하고 프록시 객체를 생성하게 되어있다.

Mockito 역시 유사한 방식으로 동작하지만, 내부적으로는 ByteBuddy를 사용하여
런타임에 프록시 객체를 생성한다.
그리고 이때 Spring과 가장 큰 차이점은 실제 객체와 연결되지 않고 Mock객체가 메서드의 호출을 자신의 선에서 처리하고 반환해준다는 점이다.

결과적으로 두 라이브러리 모두 바이트코드 조작을 통해 메서드 호출을 가로채고
동작을 제어할 수 있다는 공통점을 가진다.


마무리

테스트 코드를 작성할 때 외부 상태와 관련된 부분은 늘 테스트를 작성하기 어렵게 느껴졌다.

당시에는 Test Double이라는 개념조차 알지 못해서, 억지로 테스트 전용 클래스를 만들고 그 안에서 내가 원하는 값을 반환하도록 설정하곤 했다.

하지만 『최범균 - 테스트 주도 개발 시작하기』를 읽으며, 테스트를 위한 대역 클래스라는 Test Double의 개념을 알게 되었고, 내가 직접 만들었던 테스트 전용 클래스들 역시 결국은 모두 Test Double에 해당한다는 것을 이해하게 되었다.

테스트 코드를 작성할 때 클래스의 개수가 많아질수록 테스트 전용 클래스를 직접 만들어야 하는 일도 기하급수적으로 늘어나게 된다.
그리고 이를 해결할 수 있는 좋은 방법 중 하나로 Mockito라는 라이브러리가 있다는 것을 알게 되었고, 생소한 라이브러리의 메서드 사용법을 공부하면서 이 글을 작성하게 되었다.

아직 Mockito의 모든 메서드를 능숙하게 사용할 수 있는 것은 아니고, 모든 메서드를 다 알고 있는 것도 아니다. 하지만 기초적인 메서드들을 공부하면서 Test Double과 Mockito 라이브러리의 기본적인 사용법은 어느 정도 익힐 수 있었던 것 같다.


참고 자료

테스트 주도 개발 시작하기

profile
개발을 잘하고 싶은 사람

0개의 댓글