Software Engineering/Test & Quality

[TDD] 4. Mockito

foreverWon 2025. 8. 19. 15:27
목차

    1. Mock 객체

    • 가짜 객체(Fake Object)
    • 불필요한 의존을 제거하고 단위에 집중된 테스트 가능
    • Mockito: Mock 객체를 만들어주는 도구

    1) Stub vs Mock vs Spy

    항목 Stub Mock Spy
    목적 고정된 응답 반환 호출 여부/순서/횟수 검증, 행동 설정 가능 실제 객체를 감싸서 일부 메서드만 mocking
    구현 테스트 대상 외부 동작을 하드코딩하여 제공 미리 동작 정의하거나 호출 결과를 검증 실제 객체와 결합되어 실제 동작도 가능
    사용 시점 간단한 조건/결과가 필요한 테스트 외부 시스템이 복잡하거나 테스트 검증이 필요한 경우 일부만 대체하고 나머지는 실제 동작해야 할 때

    Stub 예시

    // Stub 예시
    EmailService stubEmailService = new EmailService() {
        @Override
        public boolean sendEmail(String to, String message) {
            return true; // 항상 성공
        }
    };
    

    Mock 예시 (Mockito 사용)

    // Mock 예시
    EmailService mockEmailService = mock(EmailService.class);
    when(mockEmailService.sendEmail(anyString(), anyString())).thenReturn(true);
    verify(mockEmailService, times(1)).sendEmail("user@example.com", "가입을 환영합니다");
    

    Spy 예시 (일부만 mocking)

    // Spy 예시
    EmailService realService = new EmailService();
    EmailService spyService = spy(realService);
    doReturn(true).when(spyService).sendEmail(anyString(), anyString());
    

    2) 실무 팁

    • Mock 객체가 너무 많아진다면, 코드 구조를 재검토 필요 (SRP 위반 가능성)
    • Mock 정의는 Fixture 또는 설정 클래스로 분리하면 재사용성이 높아짐
    • 통합 테스트에서는 실제 의존 객체를 사용하고, 단위 테스트나 슬라이스 테스트에서만 Mock을 활용하는 것이 이상적

    3) 중요 개념 정리

    항목 설명
    Mock이란? 실제 객체처럼 동작하지만 테스트에 맞게 조작 가능한 가짜 객체
    목적 외부 의존 제거, 테스트 속도 향상, 예외 상황 시뮬레이션, 단위 분리 검증
    Stub 고정된 응답만을 제공하는 단순 객체
    Spy 실제 객체에 대한 래퍼, 일부 동작만 mocking
    Mockito 가장 널리 쓰이는 Java Mocking 프레임워크, Spring과도 연동됨

    2. Mockito 기본 기능

    : Mocking Framework, 테스트 대상 클래스의 의존 객체를 가짜(Mock)로 대체하여 테스트의 정확성, 독립성, 실행 속도 보장

    1) 디렉토리 구조

    src
    ├── main
    │   └── com.example.service
    └── test
        ├── com.example.service
        └── com.example.fixture
    

    2) Mock 객체 생성 방법

    Mockito.mock() 메서드

    import static org.mockito.Mockito.mock;
    
    @Test
    void testWithMockMethod() {
        UserRepository userRepository = mock(UserRepository.class);
        // userRepository를 사용한 테스트 코드
    }
    

    @Mock 애너테이션 (권장)

    @ExtendWith(MockitoExtension.class)
    class UserServiceTest {
    
        @Mock
        private UserRepository userRepository;
    
        @Test
        void testWithMockAnnotation() {
            // userRepository는 이미 Mock 객체로 초기화되어 있음
        }
    }
    

    @MockBean 애너테이션

    @SpringBootTest
    class UserServiceIntegrationTest {
    
        @MockBean
        private UserRepository userRepository;
    
        @Autowired
        private UserService userService; // 자동으로 mockRepository가 주입됨
    }
    

    3) Mock 객체 주입 기법

    생성자 주입

    @Test
    void testWithConstructorInjection() {
        UserRepository mockRepository = mock(UserRepository.class);
        UserService userService = new UserService(mockRepository);
        // userService를 사용한 테스트
    }
    

    @InjectMocks 애너테이션 (권장)

    @ExtendWith(MockitoExtension.class)
    class UserServiceTest {
    
        @Mock
        private UserRepository userRepository;
    
        @InjectMocks
        private UserService userService;
    
        @Test
        void testWithInjectMocks() {
            // userService는 이미 mockRepository가 주입된 상태
        }
    }
    

    복잡한 의존성 구조 처리

    @ExtendWith(MockitoExtension.class)
    class OrderServiceTest {
    
        @Mock private ProductRepository productRepository;
        @Mock private UserRepository userRepository;
        @Mock private PaymentGateway paymentGateway;
    
        @InjectMocks private ProductService productService;
        @InjectMocks private UserService userService;
        @InjectMocks private OrderService orderService;
    
        @Test
        void testComplexDependencies() {
            // 복잡한 서비스-레포지토리 구조에서도 자동 주입됨
        }
    }
    

    @MockBean과 Spring Context 주입

    @SpringBootTest
    class UserServiceIntegrationTest {
    
        @MockBean
        private UserRepository userRepository;
    
        @Autowired
        private UserService userService; // 컨텍스트에서 mockRepository가 주입됨
    }
    

    4) 요약

    API 설명
    mock() 정적 메서드로 Mock 객체를 수동 생성
    @Mock 선언적으로 Mock 객체 자동 생성
    @MockBean Spring 컨텍스트에 Mock 객체 등록 (통합 테스트용)
    @InjectMocks 테스트 대상 클래스에 Mock 의존성을 자동 주입
    when() 특정 입력에 대한 Mock 동작 설정
    verify() Mock 메서드 호출 여부/횟수/순서 검증

    5) 실무 팁

    • 단위 테스트에는 @Mock + @InjectMocks 권장
    • 통합 테스트에는 @MockBean + @Autowired 권장

    3. Mock 객체 동작

    : 기본적으로 모든 메서드는 null, false, 0, 빈 컬렉션 등을 반환하므로 명시적으로 원하는 동작 설정 필요

    when().thenReturn() 구문

    // mockObject의 someMethod가 주어진 args로 호출되면, value를 반환
    when(mockObject.methodCall(arguments)).thenReturn(expectedValue);
    

    예제

    @Mock
    private UserRepository userRepository;
    
    @Test
    void should_return_user_when_email_exists() {
        User user = new User("test@example.com", "홍길동");
        when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(user));
    
        Optional result = userRepository.findByEmail("test@example.com");
    
        assertTrue(result.isPresent());
        assertEquals("홍길동", result.get().getName());
    }
    

    verify() 메서드

    verify(mockObject).methodCall(arguments);
    

    예제

    @Test
    void should_send_email_once() {
        when(emailService.sendEmail(anyString(), anyString())).thenReturn(true);
        userService.register("user@example.com");
        verify(emailService, times(1)).sendEmail("user@example.com", "가입을 환영합니다");
    }
    

    ArgumentMatchers 활용

    when(emailService.sendEmail(anyString(), eq("가입을 환영합니다"))).thenReturn(true);
    verify(emailService).sendEmail(anyString(), eq("가입을 환영합니다"));
    

    추가적인 Mock 동작 기법

    doThrow() 예외 발생 시뮬레이션

    doThrow(new RuntimeException("삭제 실패")).when(repository).deleteById(1L);
    assertThrows(RuntimeException.class, () -> repository.deleteById(1L));
    

    doNothing()

    doNothing().when(repository).deleteById(1L);
    repository.deleteById(1L);
    verify(repository).deleteById(1L);
    

    doAnswer() 커스텀 응답 설정

    doAnswer(invocation -> {
        String name = invocation.getArgument(0);
        return new User(name + "@mock.com");
    }).when(userService).createUser(anyString());
    
    User user = userService.createUser("taekho");
    assertEquals("taekho@mock.com", user.getEmail());
    

    실무 팁

    • `when().thenReturn()` → Mock 객체 상태(결과) 설정
    • `verify()` → Mock 객체 행위(호출) 검증
    • ArgumentMatchers 필수, 혼합 사용 주의
    • `verify(mock, never()).method()`로 호출되지 않음 검증 가능
    • `doThrow()`, `doAnswer()`로 예외/커스텀 시나리오 테스트 가능
    728x90