Categories: 개발일지

실용적인 테스트 가이드: @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks

테스트 시 의존성 주입(Dependency Injection)과 Mockito

Spring 애플리케이션을 개발하다 보면, 테스트 코드에서 실제 빈(Bean)을 사용하지 않고 모의 객체(Mock 객체) 로 대체해 테스트를 단순화하거나 특정 로직만 검증하고 싶을 때가 있습니다. 이러한 Mock 객체를 자동 생성/주입 해주는 기능을 제공하는 것이 대표적으로 Mockito 라이브러리입니다.

이번 글에서는 제가 수강중인 “Practical Testing: 실용적인 테스트 가이드” 강의를 통해 학습한 @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks에 대한 내용을 정리하고, 추가로 함께 진행중인 “인프런 워밍업 클럽 스터디 3기” 미션까지 공유해 보겠습니다.


  • Mockito를 이용하면 실제 인스턴스 대신 가짜(Mock) 객체나 부분적 가짜(Spy) 객체를 생성할 수 있습니다.
  • Spring Boot Test 환경에서 Mockito를 함께 사용할 때는 Spring Context에 원하는 Bean을 MockBean 또는 SpyBean으로 등록할 수도 있습니다.

아래 애너테이션들은 주로 org.mockito 패키지와 org.springframework.boot.test.mock.mockito 패키지에서 제공됩니다.

  1. @Mock / @Spy / @InjectMocks
    • Mockito에서 제공
    • Spring Context(빈 컨테이너)에 직접적 영향 없이, 순수 단위 테스트(Unit Test) 혹은 스프링이 뜨지 않는 상태에서 쓰는 경우에 주로 활용
  2. @MockBean / @SpyBean
    • Spring Boot Test에서 제공
    • 실제로 Spring Context가 뜨는 통합 테스트(Integration Test) 환경에서 Mock 또는 Spy 빈을 Spring ApplicationContext에 등록해야 할 때 사용

테스트 요구사항과 상황에 따라 위 애너테이션들을 적절히 조합합니다.

@Mock

개념
  • @Mock은 Mockito 라이브러리에서 제공하는 애너테이션입니다.
  • 가짜(Mock) 객체를 생성하여, 실제 객체를 사용하지 않고도 해당 객체의 메소드 호출, 반환값 등을 컨트롤하면서 테스트할 수 있게 합니다.
  • 단위 테스트 환경(스프링 컨텍스트 없이)에서 가장 많이 볼 수 있습니다.
동작 방식
  • @Mock을 사용하면, Mockito가 가짜 객체를 만들어냅니다.
  • 이 가짜 객체는 기본적으로 아무 동작도 하지 않고, 호출 시에 항상 기본값(객체면 null, 숫자면 0, boolean은 false 등)을 반환합니다.
  • when(…).thenReturn(…) 혹은 doReturn(…).when(…) 형태로 반환값을 미리 세팅해놓을 수 있습니다.
  • 메소드 실행 시 내부 구현체가 동작하지 않기 때문에, 해당 Mock이 의존하는 외부 의존성이나 DB 등은 전혀 호출되지 않습니다.
예시 코드
Java
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;

class MockTest {

    @Mock
    private UserRepository userRepository; // 가짜(Mock) 객체

    @Test
    void testFindUserWithMock() {
        // @Mock 애너테이션이 동작하도록 초기화
        MockitoAnnotations.openMocks(this);

        // 가짜 객체이므로, 직접 동작을 세팅해야 함
        when(userRepository.findUsernameById(1L)).thenReturn("MockUser");

        // 실제 호출
        String username = userRepository.findUsernameById(1L);

        // 검증
        assertEquals("MockUser", username);
    }
}
Java

  • MockitoAnnotations.openMocks(this) 호출은 @Mock을 통해 선언된 필드를 초기화합니다.
  • when(…).thenReturn(…)을 통해 원하는 반환값을 지정했습니다.
정리
  • @Mock은 스프링 Context를 사용하지 않는 순수한 단위 테스트에서 모의 객체를 생성할 때 자주 쓴다.
  • 외부 의존성을 완전히 배제하고 특정 로직을 테스트하거나, 복잡한 객체를 직접 생성하기 어렵거나 원하지 않을 때 사용한다.

@MockBean

개념
  • @MockBean은 Spring Boot Test에서 제공하는 애너테이션입니다.
  • Mock 객체를 Spring ApplicationContext에 빈(Bean) 으로 등록하여, 실제 프로젝트 코드에서 @Autowired 또는 @Inject로 주입받는 대상 빈을 Mock 객체로 교체합니다.
  • 따라서, 스프링이 뜨는 테스트(예: @SpringBootTest, @WebMvcTest 등)에서 특정 Bean을 가짜 객체로 대체하여 테스트하고 싶을 때 사용합니다.
동작 방식
  • 테스트 클래스에 @MockBean으로 선언된 필드는 Spring Context 내에 동일 타입의 Bean이 존재한다면 이를 가짜 객체로 교체합니다.
  • 다른 Bean들이 해당 Bean을 의존하고 있다면, 실제 객체 대신 Mock 객체를 주입받게 됩니다.
  • 스프링 컨텍스트가 뜨므로, 다른 Bean들은 그대로 사용하되, 일부 Bean만 Mock으로 교체할 수 있어 통합 테스트와 단위 테스트의 중간 단계 역할을 합니다.
예시 코드
Java
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
class MockBeanTest {

    @MockBean
    private UserRepository userRepository; // Spring Context 내 실제 Bean을 이 Mock으로 교체

    @Test
    void testFindUserWithMockBean() {
        // MockBean은 Spring Boot Test에서 자동으로 초기화되므로 openMocks 불필요
        when(userRepository.findUsernameById(1L)).thenReturn("MockBeanUser");

        String username = userRepository.findUsernameById(1L);

        assertEquals("MockBeanUser", username);
    }
}
Java

  • @MockBean을 붙여 선언한 UserRepository는 실제 구현체 대신 Mock 객체로 교체됩니다.
  • @SpringBootTest 등으로 스프링이 기동되지만, UserRepository만 가짜로 치환되어 다른 빈들이 의존해도 그 의존성은 Mock이 됩니다.
정리
  • @MockBean은 스프링 통합 테스트에서 특정 Bean만 Mock으로 교체해야 할 때 유용하다.
  • 예를 들어, DB 접근 로직이 담긴 Repository나 외부 API 호출을 담당하는 Service를 Mock으로 교체하고, 나머지 Bean들은 실제 동작을 수행하게 하여 스프링 환경을 전체적으로 테스트하면서 특정 부분 의존성은 제거할 수 있다.

@Spy

개념
  • @Spy도 Mockito 라이브러리에서 제공하는 애너테이션입니다.
  • Spy 객체는 실제 객체를 부분적으로 감시(Spy)하면서, 일부 메소드는 진짜 구현을 호출하고, 일부 메소드는 Mock처럼 동작하게 설정할 수 있습니다.
  • 즉, Mock보다 더 정밀하게 설정이 가능하며, 기본적으로는 실제 객체처럼 동작하되 필요한 부분만 Stub(가짜 동작 설정)할 수 있습니다.
동작 방식
  • @Spy가 붙으면, Mockito가 실제 객체를 생성해두고 이 객체에 대한 Proxy를 만듭니다.
  • 별도 설정을 하지 않은 메소드를 호출하면 실제 메소드 로직이 실행됩니다.
  • Mockito when(…).thenReturn(…) 구문 등으로 스파이 객체의 특정 메소드를 가짜로 동작하게 만들 수도 있습니다.
예시 코드
Java
import org.junit.jupiter.api.Test;
import org.mockito.Spy;
import org.mockito.MockitoAnnotations;

import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;

class SpyTest {

    static class SampleService {
        public String sayHello(String name) {
            return "Hello " + name;
        }
    }

    @Spy
    private SampleService sampleService;

    @Test
    void testSpy() {
        MockitoAnnotations.openMocks(this);

        // 원래는 실제 로직을 호출하지만, 특정 인자일 때는 가짜 반환값을 설정
        when(sampleService.sayHello("Spy")).thenReturn("Hello Spy (stubbed)");

        // 1) "Spy" 인자로 부르는 경우는 Stub값 사용
        assertEquals("Hello Spy (stubbed)", sampleService.sayHello("Spy"));

        // 2) 그 외 인자는 실제 메소드 호출
        assertEquals("Hello John", sampleService.sayHello("John"));
    }
}
Java

  • SampleService는 실제로 존재하는 구현체입니다.
  • @Spy로 선언해주었고, MockitoAnnotations.openMocks(this)로 스파이 객체 초기화 후에 when(…)을 통해 특정 경우만 모의 동작을 지정합니다.
  • 그 외 인자에는 진짜 sayHello() 메소드를 호출합니다.
정리
  • @Spy는 실제 로직을 일부 유지해야 하거나, 특정 메소드만 Mock처럼 동작시키고 싶을 때 사용.
  • 완전한 가짜 객체를 원하면 @Mock을, 부분 모의가 필요하면 @Spy를 선택.

@SpyBean

개념
  • @SpyBean은 @MockBean과 유사하게 Spring Boot Test에서 제공하는 애너테이션입니다.
  • 실제로 Spring Context에 올라가는 Bean을 Spy 객체로 치환합니다.
동작 방식
  • @SpyBean이 선언된 필드는 애플리케이션 컨텍스트의 Bean으로 등록될 때, Spy 객체가 주입됩니다.
  • 즉, 스프링 전반이 실제 Bean으로 동작하되, @SpyBean이 지정된 Bean은 실제 구현체 + Spy 동작을 수행하게 됩니다.
  • 필요 시 특정 메소드는 Stub 처리가 가능하며, 그 외는 실제 로직을 그대로 수행합니다.
예시 코드
Java
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;

import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
class SpyBeanTest {

    @SpyBean
    private SampleService sampleService; // Spring Bean을 Spy로 치환

    @Test
    void testSpyBean() {
        // Spring Boot Test에서 자동 초기화됨
        when(sampleService.sayHello("SpyBean")).thenReturn("Hello SpyBean (stubbed)");

        // 1) "SpyBean" 인자로 부르는 경우는 Stub값
        assertEquals("Hello SpyBean (stubbed)", sampleService.sayHello("SpyBean"));

        // 2) 그 외 인자는 실제 서비스 로직 호출
        assertEquals("Hello John", sampleService.sayHello("John"));
    }
}
Java

  • SampleService가 원래 Spring Bean으로 등록되어 있지만, 테스트에서는 @SpyBean이 붙어 Spy 객체로 대체되어 실행됩니다.
  • 만약 서비스 내부 로직 일부만 모의로 처리하고 싶다면, @SpyBean이 편리합니다.
정리
  • @SpyBean을 통해 스프링 통합 테스트에서 특정 Bean에 대해서만 실제 로직 + 부분 모의가 가능.
  • 특정 메소드는 진짜 동작, 특정 메소드는 Stub 처리를 통해 테스트가 가능.

@InjectMocks

개념
  • @InjectMocks는 Mockito에서 제공하는 애너테이션입니다.
  • @Mock 또는 @Spy로 생성한 가짜 객체(필드)들을 자동으로 주입(Injection) 해주는 역할을 합니다.
  • 예: 테스트 대상 클래스가 여러 의존성을 가지고 있을 때, @InjectMocks를 사용하면 @Mock 또는 @Spy로 선언된 필드들이 자동으로 해당 의존성 주입 방식에 맞춰 넣어집니다.
동작 방식
  • @InjectMocks가 붙은 객체를 생성할 때, Mockito는 해당 객체가 가지는 생성자, 세터, 필드 등을 분석하여 @Mock/@Spy로 선언된 필드를 주입합니다.
  • 일반적으로 아래와 같은 우선순위를 갖고 주입합니다.
    1. 생성자 주입
    2. 세터 주입
    3. 필드 주입
예시 코드
Java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;

class InjectMocksTest {

    static class UserService {
        private final UserRepository userRepository;

        public UserService(UserRepository userRepository) {
            this.userRepository = userRepository;
        }

        public String getUsername(Long id) {
            return userRepository.findUsernameById(id);
        }
    }

    interface UserRepository {
        String findUsernameById(Long id);
    }

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService; // userRepository 주입 대상

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

    @Test
    void testInjectMocks() {
        when(userRepository.findUsernameById(1L)).thenReturn("InjectMockUser");

        String username = userService.getUsername(1L);

        assertEquals("InjectMockUser", username);
    }
}
Java

  • UserService는 내부에서 UserRepository를 의존하고 있습니다.
  • @Mock으로 선언된 userRepository를, @InjectMocks로 선언된 userService에 자동으로 주입됩니다.
  • 테스트 시 userService를 직접 생성할 필요 없이 Mockito가 알아서 new UserService(userRepository) 형태로 의존성을 연결합니다.

미션

미션 1: @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 차이 정리

위 본문에서 설명하였으니, 간략히 한 표로 정리해봅니다.

애노테이션주로 사용 환경스프링 빈 등록 여부특징
@MockMockito (단위 테스트)X (스프링 컨텍스트와 무관)순수 Mock 객체. 내부 로직 없이 호출 기록 및 Stubbing에 특화
@MockBeanSpring Boot TestO (스프링 빈을 Mock 객체로 교체)실제 스프링 빈을 Mock으로 대체. 슬라이스/통합테스트 등에서 활용
@SpyMockito (단위 테스트)X (스프링 컨텍스트와 무관)Spy 객체. 기본적으로 실제 메서드 동작 + 필요한 부분만 가짜(Stubbing)
@SpyBeanSpring Boot TestO (스프링 빈을 Spy 객체로 교체)실제 스프링 빈을 Spy로 대체. 실제 로직이 동작하되, 부분적 모의 가능
@InjectMocksMockito (단위 테스트)@Mock/@Spy 객체를 대상 클래스 내부 필드에 자동으로 주입

미션 2: 각 항목을 @BeforeEach, given절, when절에 어떻게 배치할것인가

@BeforeEach를 사용하지 않고, 각 테스트 메서드 내부에서 필요한 데이터를 독립적으로 생성하는 구조로 구성했습니다. 이는 테스트마다 서로 다른 시나리오를 유연하게 구성하기 위해, 공통 데이터 생성을 한곳에 몰아넣지 않는 방식입니다.

Java
@BeforeEach
void init() {
    // 공통 데이터를 미리 생성하지 않습니다.
    /*
     * 각 테스트가 각각의 상황(사용자, 게시물, 댓글)을 자유롭게 구성할 수 있도록,
     * 여기서는 사전에 아무것도 세팅해두지 않습니다.
     * 테스트마다 필요한 데이터 타입이나 조건이 다를 수 있으므로,
     * 각 테스트 메서드 내부에서 직접 객체를 생성하고 준비해 주는 방식을 채택했습니다.
     */
}

@DisplayName("사용자가 댓글을 작성할 수 있다.")
@Test
void writeComment() {
    // given
    // 1-1. 사용자 생성에 필요한 내용 준비
    // 1-2. 사용자 생성
    // 1-3. 게시물 생성에 필요한 내용 준비
    // 1-4. 게시물 생성
    // 1-5. 댓글 생성에 필요한 내용 준비

    // when
    // 1-6. 댓글 생성

    // then
    // 검증
}

@DisplayName("사용자가 댓글을 수정할 수 있다.")
@Test
void updateComment() {
    // given
    // 2-1. 사용자 생성에 필요한 내용 준비
    // 2-2. 사용자 생성
    // 2-3. 게시물 생성에 필요한 내용 준비
    // 2-4. 게시물 생성
    // 2-5. 댓글 생성에 필요한 내용 준비
    // 2-6. 댓글 생성

    // when
    // 2-7. 댓글 수정

    // then
    // 검증
}

@DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
@Test
void cannotUpdateCommentWhenUserIsNotWriter() {
    // given
    // 3-1. 사용자1 생성에 필요한 내용 준비
    // 3-2. 사용자1 생성
    // 3-3. 사용자2 생성에 필요한 내용 준비
    // 3-4. 사용자2 생성
    // 3-5. 사용자1의 게시물 생성에 필요한 내용 준비
    // 3-6. 사용자1의 게시물 생성
    // 3-7. 사용자1의 댓글 생성에 필요한 내용 준비
    // 3-8. 사용자1의 댓글 생성

    // when
    // 3-9. 사용자2가 사용자1의 댓글 수정 시도

    // then
    // 검증
}
Java

왜 이렇게 구성하는가?
  • 유연한 시나리오 구성
    각 테스트마다 필요한 데이터 조합(사용자1·게시물·댓글 / 사용자2 추가 등)이 달라질 경우, @BeforeEach에서 공통 데이터를 미리 만들어두면 오히려 특정 테스트에 맞게 커스터마이징하기 까다로워질 수 있습니다.
  • 독립성
    모든 테스트가 “자기만의 데이터”를 직접 준비하므로, 한 테스트의 변경이 다른 테스트에 영향을 주지 않습니다.
  • 가독성
    테스트 메서드 내부를 보면, 어떤 사용자와 어떤 게시물을 만들어서 댓글을 작성(또는 수정)하는지 직접 확인할 수 있습니다.

물론, 테스트들 간에 완전히 똑같이 필요한 데이터가 반복된다면, 그 부분은 @BeforeEach로 추출해 중복을 줄이는 방법도 있습니다. 그러나 “테스트별로 크게 달라지는 시나리오”일 땐, 위와 같은 독립 구성 방식이 더 직관적이고 유연하다고 판단했습니다.

활용 시 주의사항

  1. 너무 복잡한 테스트
    • @MockBean이나 @SpyBean을 과도하게 사용하여 Mock 객체가 많아지면, 테스트가 복잡해지고 유지보수가 어려워집니다.
    • 테스트 범위를 명확히 하고, 필요한 부분만 Mock/Spy로 대체하는 것이 좋습니다.
  2. Mock 객체 내부 구현 의존
    • Mocking/Stubbing을 잘못 설계하면, 실제 구현보다 Mock 설정에 더 의존적이 되어버려 소스 코드 리팩토링이 어려워질 수 있습니다.
    • public API 위주로 Mock 동작을 지정하고, 내부 구현 로직까지 Mock나 Spy에 강하게 의존하지 않도록 주의합니다.
  3. @InjectMocks의 생성자 주입 순서
    • 생성자, 세터, 필드 순으로 주입하므로 의도한 방식과 일치하는지 확인합니다.
    • Lombok이나 여러 생성자가 있는 경우, 주입 경로가 의도와 다를 수도 있으니 주의해야 합니다.
  4. 스프링 컨텍스트 로딩 속도
    • @MockBean, @SpyBean을 사용하는 통합 테스트는 스프링 컨텍스트를 로딩하기 때문에 속도가 느릴 수 있습니다.
    • 단위 테스트통합 테스트를 구분해 설계하여, 빈번한 테스트는 단위 테스트로 빠르게 돌리고, 최종 통합 검증 단계에서만 스프링을 기동하는 방식을 고려하세요.
  5. 같은 타입 Bean 여러 개 존재 시
    • @MockBean, @SpyBean 사용 시 동일한 타입의 Bean이 여러 개 컨텍스트에 등록되어 있으면, 어느 Bean이 대체되는지 명확히 파악해야 합니다.
    • name 속성, qualifier 등을 통해 원하는 Bean에 명시적으로 Mock 또는 Spy를 적용하는 것이 좋습니다.

마무리

테스트 코드를 작성할 때, MockSpy 개념을 적절히 활용하면 실제 외부 의존성 없이도 독립적이고 유연하게 테스트를 진행할 수 있습니다.

  • 단위 테스트에서는 @Mock/@Spy/@InjectMocks를 통해 빠르고 간편하게 가짜 객체를 만들고, 필요한 부분만 Stubbing하여 원하는 로직을 검증할 수 있습니다.
  • 스프링 컨텍스트를 사용하는 통합 테스트에서는 @MockBean/@SpyBean으로 실제 Bean을 가짜(또는 부분 가짜)로 교체하여, 애플리케이션의 큰 흐름을 테스트하되 특정 부분만 모의화할 수 있습니다.

결국, 테스트 설계의 핵심은 “테스트하고자 하는 대상 로직을 최대한 명확히 드러내면서, 불필요한 의존성을 제거하여 빠르고 안정적인 검증을 가능케 하는 것”이라 생각합니다.

위의 내용을 바탕으로, 본인 프로젝트나 업무 환경에 맞춰 다양한 방식의 테스트 전략을 시도해 보시길 바랍니다. “Practical Testing: 실용적인 테스트 가이드” 강의와 “인프런 워밍업 클럽 스터디 3기”에서 다뤘던 내용을 공유했는데, 실제로 코드에 적용해보면 테스트 품질과 개발 효율이 한층 높아질 것이라 생각합니다.

출처
suover

Recent Posts

실용적인 테스트 가이드: Layered Architecture 레이어드 아키텍처

들어가며 스프링 기반 프로젝트에서 좋은 설계 구조와 테스트 전략은 소프트웨어 품질과 유지보수성에 직결됩니다. 최근 학습한…

1주 ago

읽기 좋은 코드를 작성하는 사고법: 논리, 사고의 흐름과 SOLID

들어가며 코드를 작성할 때 종종 "이 로직을 어떻게 단순하고 읽기 쉽게 표현할 수 있을까?" 고민하게…

4주 ago

읽기 좋은 코드를 작성하는 사고법: 추상과 구체

들어가며 코드를 작성하다 보면 "왜 이 코드는 한눈에 이해가 안 될까?" 하는 고민을 종종 하게…

4주 ago

HTTP 상태코드 총정리 서버-클라이언트 간 명확한 의사소통

HTTP 상태코드란 무엇인가? HTTP 상태코드(HTTP Status Code)는 서버가 클라이언트의 요청을 처리한 결과를 수치화된 코드로 나타내는…

1개월 ago

HTTP 메서드 완벽 가이드 GET, POST, PUT, PATCH, DELETE 등 총정리

HTTP란 무엇인가? HTTP(Hypertext Transfer Protocol)는 웹에서 데이터를 주고받기 위해 사용하는 응용 계층 프로토콜입니다. 우리가 브라우저에서…

1개월 ago

HTTP 기본과 특징 알아둬야 할 모든 것

HTTP란 무엇인가? HTTP(Hypertext Transfer Protocol)는 인터넷에서 웹 브라우저와 웹 서버가 서로 통신하기 위해 사용하는 프로토콜입니다.…

2개월 ago