백기선 님의 인프런 강의 "더 자바, 애플리케이션을 테스트하는 다양한 방법"를 정리한 글입니다.
1. 소개
- Java 계열에서 가장 많이 사용되는 Mock 프레임워크 (50%+)
구성 요소
- Mock: 실제 객체처럼 동작하지만, 프로그래머가 행동을 제어할 수 있는 객체
- Mockito: Mock 객체를 쉽게 만들고, 조작하고, 검증할 수 있는 프레임워크
대체제
- EasyMock
- JMock
2. Mockito 시작하기
Gradle
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
    testImplementation 'org.mockito:mockito-core:5.12.0'
    testImplementation 'org.mockito:mockito-junit-jupitor:5.12.0'
}
Spring Boot 2.2+
- spring-boot-starter-test에 자동 포함됨
3. 적용
Mock 객체 생성
예제) Mockito.mock()
더보기
MemberService memberService = mock(MemberService.class);
StudyRepository studyRepository = mock(StudyRepository.class);
예제) @Mock
더보기
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
    @Mock MemberService memberService;
    @Mock StudyRepository studyRepository;
}
- @ExtendWith(MockitoExtension.class)와 함께 사용
- 필드 또는 메서드 매개변수로 가능
예제) 메소드 매개변수 이용
더보기
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
    @Test
    void createStudyService(@Mock MemberService memberService,
                            @Mock StudyRepository studyRepository) {
        StudyService studyService = new StudyService(memberService, studyRepository);
        assertNotNull(studyService);
    }
}
Mock 객체 동작 정의 (Stubbing)
Stubbing API
| 기능 | 설명 | 
| 일반 Stubbing | 특정 호출에 대해 값을 리턴하도록 설정 | 
| 예외 던지기 | 호출 시 예외를 던지도록 설정 | 
| 연속 호출 Stubbing | 연속 호출 시 각각 다른 값을 리턴하도록 설정 | 
| void 메서드 예외 던지기 | void 메서드가 호출될 때 예외를 던지도록 설정 | 
| void 메서드 실행 없이 무시 | void 메서드는 기본적으로 아무 일도 하지 않음 | 
| 메서드 호출 시 특정 동작 수행 | 호출 시 사용자 정의 동작 수행 가능 | 
| 호출된 값에 따라 다르게 동작 | 특정 인자에 대해 다르게 반환 | 
| Argument Matcher 활용 | 인자 매처 (any(), eq(), 등) 사용 가능 | 
예제
더보기
@ExtendWith(MockitoExtension.class)
public class MockitoTest {
    @Mock
    UserService mock;
    @Test
    void when_thenReturn() {
        when(mock.getName()).thenReturn("mockito");
        assertEquals(mock.getName(), "mockito");
        when(mock.greet("Tom")).thenReturn("Hi Tom");
        assertEquals(mock.greet("Tom"), "Hi Tom");
        when(mock.greet(anyString())).thenReturn("Hello!");
        assertEquals(mock.greet("mockito"), "Hello!");
        doAnswer(inv -> {
            System.out.println("doAnswer 메서드 호출됨!");
            return "doAnswer";
        }).when(mock).getName();
        assertEquals(mock.getName(), "doAnswer");
    }
    @Test
    void when_ThenReturn_ConsecutiveCall() {
        when(mock.getCount()).thenReturn(1, 2, 3);
        assertEquals(mock.getCount(), 1);
        assertEquals(mock.getCount(), 2);
        assertEquals(mock.getCount(), 3);
    }
    @Test
    void when_thenThrow() {
        when(mock.getName()).thenThrow(new IllegalStateException("예외 발생!"));
        assertThrows(IllegalStateException.class, () -> {mock.getName();});
        doThrow(new IllegalStateException()).when(mock).getName();
        assertThrows(IllegalStateException.class, () -> {mock.getName();});
    }
}
기본 동작 (Mock을 생성했지만 Stubbing하지 않았을 때)
| 타입 | 기본 반환값 | 설명 | 
| 객체 (reference type) | null | 일반 객체 타입은 기본적으로 null 반환 | 
| Optional<T> | Optional.empty() | Optional 타입은 비어 있는 Optional 반환 | 
| 기본형 (int, boolean 등) | 0, false, 0.0f, 0.0d, etc | 기본형 타입은 해당 타입의 기본값 반환 | 
| 컬렉션 (List, Set 등) | 빈 컬렉션 ([], {} 등) | 리스트, 세트 등은 비어 있는 컬렉션 반환 | 
| void 메서드 | 아무 일도 안 함 | 예외 없이 그냥 실행됨 (noop) | 
예제
더보기
@ExtendWith(MockitoExtension.class)
public class NoStubbingTest {
    @Mock
    UserService userService;
    @Test
    void defaultBehavior() {
        assertNull(userService.getName());
        assertEquals(Optional.empty(), userService.getOptionalName());
        assertEquals(0, userService.getAge());
        assertFalse(userService.isActive());
        assertTrue(userService.getRoles().isEmpty());
        userService.logout();
    }
}
Mock 객체 사용 검증 (Verify)
| 기능 | 메서드 / API | 설명 | 
| 호출 횟수 확인 | verify(mock, times(n)) | 정확히 n번 호출되었는지 확인 | 
| verify(mock, atLeastOnce()) | 최소 한 번 이상 호출되었는지 확인 | |
| verify(mock, never()) | 전혀 호출되지 않았는지 확인 | |
| 호출 순서 확인 | InOrder inOrder = inOrder(...) | 여러 Mock의 메서드가 특정 순서로 호출되었는지 확인 | 
| 호출 타이밍 검증 | verify(mock, timeout(ms)) | 지정한 시간 내에 호출되었는지 확인 (ms 밀리초 단위) | 
| 불필요한 호출 확인 | verifyNoMoreInteractions(...) | 지정된 호출 외에 다른 호출이 없었는지 확인 (불필요한 호출 확인용) | 
예제
더보기
@Test
void verify_call_times() {
    mock.getName();
    mock.getName();
    verify(mock, times(2)).getName();
    verify(mock, atLeastOnce()).getName();
    verify(mock, never()).greet("mockito");
}
@Test
void verify_inorder() {
    UserService mock1 = mock(UserService.class);
    UserService mock2 = mock(UserService.class);
    mock1.getName();
    mock2.getCount();
    InOrder inOrder = inOrder(mock1, mock2);
    inOrder.verify(mock1).getName();
    inOrder.verify(mock2).getCount();
}
@Test
void verify_timeout() {
    new Thread(() -> {
        try {
            Thread.sleep(50);
            mock.getCount();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
    verify(mock, timeout(100)).getCount();
}
@Test
void verify_no_more_interactions() {
    mock.getCount();
    verify(mock).getCount();
    verifyNoMoreInteractions(mock);
}
4. BDD Mockito
BDD
- 행위 중심 개발 (Behavior-Driven Development)
- 소프트웨어가 어떻게 행동해야 하는지를 중심으로 개발을 이끌어가는 방법
- 사용자의 관점에서 기능을 정의하고, 그 정의를 바탕으로 테스트를 작성함
핵심 요소
| 구성 요소 | 설명 | 
| Story | 사용자 시나리오 중심 설명 (Narrative) | 
| Given | 어떤 조건이 주어졌을 때 (상태, 전제조건 등) | 
| When | 어떤 행동(이벤트)이 일어나면 | 
| Then | 어떤 결과가 나와야 한다 | 
BDD API
| 역할 | BDD API | 설명 | 
| Stubbing (Given) | given(mock.method()).willReturn(value) | 특정 호출이 일어날 때 값을 반환하도록 설정 | 
| given(mock.method()).willThrow(exception) | 호출 시 예외를 던지도록 설정 | |
| willAnswer(answer).given(mock).method(...) | 호출 시 사용자 정의 동작을 실행 | |
| Verification (Then) | then(mock).should().method() | 특정 메소드가 호출됐는지 검증 | 
| then(mock).should(times(n)).method() | 특정 메소드가 n번 호출됐는지 검증 | |
| then(mock).shouldHaveNoMoreInteractions() | 더 이상의 상호작용이 없었는지 검증 | |
| then(mock).should(never()).method() | 특정 메소드가 절대 호출되지 않았는지 검증 | |
| 호출 순서 검증 | InOrder inOrder = inOrder(mock1, mock2) | 여러 mock 간 호출 순서 검증 시작 | 
| then(mock).should(inOrder).method() | 특정 호출 순서를 따라 호출되었는지 검증 | |
| 호출 타이밍 검증 | then(mock).should(timeout(100)).method() | 100ms 이내에 호출되었는지 확인 | 
| 더 이상 호출되지 않음 | then(mock).shouldHaveNoMoreInteractions() | 더 이상의 호출이 없었는지 확인 | 
예제
더보기
@Test
void bdd_given_willReturn() {
    given(mock.getName()).willReturn("mockito");
    assertEquals(mock.getName(), "mockito");
}
@Test
void bdd_given_willThrow() {
    given(mock.getName()).willThrow(new IllegalStateException("예외 발생!"));
    assertThrows(IllegalStateException.class, () -> {mock.getName();});
}
@Test
void bdd_should() {
    mock.getCount();
    then(mock).should().getCount();
}
@Test
void bdd_should_times() {
    mock.getCount();
    mock.getCount();
    then(mock).should(times(2)).getCount();
}
@Test
void bdd_should_have_no_more_interactions() {
    mock.getCount();
    mock.getCount();
    then(mock).should(times(2)).getCount();
    then(mock).shouldHaveNoMoreInteractions();
}
@Test
void bdd_should_another_method() {
    mock.getCount();
    then(mock).should(never()).getName();
}
@Test
void bdd_should_order() {
    UserService mock1 = mock(UserService.class);
    UserService mock2 = mock(UserService.class);
    mock1.getName();
    mock2.getCount();
    InOrder inOrder = inOrder(mock1, mock2);
    then(mock1).should(inOrder).getName();
    then(mock2).should(inOrder).getCount();
}
@Test
void bdd_should_timeout() {
    new Thread(() -> {
        try {
            Thread.sleep(50);
            mock.getCount();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
    then(mock).should(timeout(100)).getCount();
}
출처
'Code > Test' 카테고리의 다른 글
| [더 자바, 애플리케이션을 테스트하는 다양한 방법] 1. JUnit 5 (0) | 2025.04.19 | 
|---|---|
| [단위 테스트] 9. 목 처리에 대한 모범 사례 (0) | 2025.03.01 | 
| [단위 테스트] 8-2. 통합 테스트를 하는 이유: 인터페이스 (1) | 2025.03.01 | 
| [단위 테스트] 8-1. 통합 테스트를 하는 이유: 통합 테스트 (0) | 2025.02.28 | 
| [단위 테스트] 7-2. 가치 있는 단위 테스트를 위한 리팩터링: 감사 시스템 (0) | 2025.02.28 |