🎁[속보] 인프런 내 깜짝 선물 출현 중🎁

[워밍업 클럽 3기 BE 클린코드&테스트] - Day 18 미션

1. @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks 의 차이를 한번 정리해 봅시다.

1. @Mock vs @MockBean

@Mock

  • Mockito에서 제공하는 애너테이션.

  • 해당 필드에 가짜(Mock) 객체를 생성해 준다.

  • 주로 순수한 단위 테스트(단일 클래스 테스트)에서 사용한다.

  • 스프링 컨테이너와는 무관하게 동작하므로, 원하는 경우 @InjectMocks를 통해 직접 의존성 주입(필드/생성자/Setter)이 일어날 수 있게끔 설정해줘야 한다.

ExtendWith(SpringExtension.class)
class MyServiceTest {

    // 1) Repository를 Mock 객체로 만든다.
    @Mock
    private MyRepository myRepository;
    
    // 2) MyRepository가 필요한 MyService에 @InjectMocks로 Mock을 주입받는다.
    @InjectMocks
    private MyService myService;

    @Test
    void testFindById() {
        // given
        when(myRepository.findById(1L)).thenReturn(Optional.of(new MyEntity(1L, "Test Data")));

        // when
        MyEntity result = myService.findById(1L);

        // then
        assertThat(result.getName()).isEqualTo("Test Data");
    }
}

@MockBean

  • Spring Boot에서 제공하는 애너테이션 (org.springframework.boot.test.mock.mockito).

  • 스프링 컨테이너에 Mock 객체로 등록해준다. 즉, @Autowired나 다른 Bean들이 해당 Mock 객체를 주입받는다.

  • 주로 @SpringBootTest, @WebMvcTest스프링 컨텍스트가 필요한 통합 테스트에서 사용한다.

  • 실제 Bean 대신 MockBean이 컨테이너에서 주입되는 형태이다.

@SpringBootTest
class MyServiceIntegrationTest {

    @Autowired
    private MyService myService;

    // 스프링 컨테이너에 MyRepository를 MockBean으로 등록
    @MockBean
    private MyRepository myRepository;

    @Test
    void testFindById() {
        // given
        when(myRepository.findById(1L)).thenReturn(Optional.of(new MyEntity(1L, "Integration Test")));

        // when
        MyEntity result = myService.findById(1L);

        // then
        assertThat(result.getName()).isEqualTo("Integration Test");
    }
}

2. @Spy vs @SpyBean

@Spy

  • Mockito에서 제공하는 애너테이션.

  • 부분(Mock) 테스트에 사용되며, 실제 객체를 부분적으로 Mocking한다.

  • 즉, 스파이(Spy)는 기본적으로 진짜 메서드를 실행하지만, 원하는 메서드만 when(...).thenReturn(...) 형태로 오버라이딩해서 결과를 바꿀 수 있다.

  • @Mock과의 차이는 @Spy는 기본 동작이 실제 객체라는 점이다.

@ExtendWith(SpringExtension.class)
class MyServiceSpyTest {

    // Repository 스파이 객체
    // => 실제 MyRepository 구현체 인스턴스를 생성 + 원하는 부분만 stub 가능
    @Spy
    private MyRepository myRepository = new MyRepositoryImpl(); 

    // 스파이 객체를 주입받을 MyService
    @InjectMocks
    private MyService myService;

    @Test
    void testFindByIdWithSpy() {
        // spy는 실제 동작을 수행하므로,
        // 특정 메서드만 오버라이딩해서 동작을 가짜로 만들 수 있다.
        doReturn(Optional.of(new MyEntity(2L, "Spy Data")))
                .when(myRepository).findById(2L);

        // 2L의 경우에는 Stub이 적용되어 Mock 동작,
        // 1L의 경우에는 실제 MyRepositoryImpl 동작.
        MyEntity result1 = myService.findById(1L); // 실제동작
        MyEntity result2 = myService.findById(2L); // Stub으로 오버라이딩

        assertThat(result1).isNotNull(); // 실제 Repo 동작 결과
        assertThat(result2.getName()).isEqualTo("Spy Data"); // 오버라이딩된 결과
    }
}

@SpyBean

  • Spring Boot에서 제공.

  • @MockBean과 유사하지만, 실제 스프링 Bean을 스파이로 만들어 컨테이너에 등록한다.

  • 즉, 컨테이너가 관리하는 Bean을 부분 Mocking하고 싶은 경우에 사용.

  • 다른 Bean들이 해당 @SpyBean을 주입받아 사용할 때는, 기본적으로 진짜 로직을 타게 되지만 원하는 메서드만 Stub 할 수 있다.

@SpringBootTest
class MyServiceSpyBeanTest {

    // MyService는 진짜 Bean
    @Autowired
    private MyService myService;

    // MyRepository를 스파이 Bean으로 등록
    @SpyBean
    private MyRepository myRepository;

    @Test
    void testFindByIdWithSpyBean() {
        // 실제 Bean이지만 특정 메서드를 Stub
        doReturn(Optional.of(new MyEntity(999L, "SpyBean Data")))
            .when(myRepository).findById(999L);

        MyEntity result = myService.findById(999L);

        // 999L일 때만 Stub 동작 → "SpyBean Data"
        assertThat(result.getName()).isEqualTo("SpyBean Data");
    }
}

3. @InjectMocks

  • Mockito에서 제공하는 애너테이션.

  • @Mock 또는 @Spy로 만들어진 Mock/Spy 객체들을 해당 클래스(필드, 생성자 등)에 자동 주입해 준다.

  • 스프링과는 무관하게 Mockito 레벨에서만 동작하며, 생성자 주입/필드 주입/Setter 주입 방식으로 의존성을 주입해 준다.

@ExtendWith(SpringExtension.class)
class MyServiceTest {

    @Mock
    private MyRepository myRepository;

    // MyService 생성 시 @Mock으로 만든 myRepository가 자동 주입됨
    @InjectMocks
    private MyService myService;

    @Test
    void testSomeLogic() {
        // ...
    }
}

2. 아래 3개의 테스트가 있습니다.

내용을 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치하고 싶으신가요?

(@BeforeEach에 올라간 내용은 공통 항목으로 합칠 수 있습니다. ex. 1-1과 2-1을 하나로 합쳐서 @BeforeEach에 배치)


/***
 * ✔️ 게시판 게시물에 달리는 댓글을 담당하는 Service Test
 * ✔️ 댓글을 달기 위해서는 게시물과 사용자가 필요하다.
 * ✔️ 게시물을 올리기 위해서는 사용자가 필요하다."
 *
 */
class Test {

    @BeforeEach
    void setUp() {
        // 사용자 생성에 필요한 내용 준비 (1-1, 2-1, 3-1)
        // 사용자 생성 (1-2, 2-2, 3-2)

        // 게시물 생성에 필요한 내용 준비 (1-3, 2-3, 3-5)
        // 게시물 생성 (1-4, 2-4, 3-6)
    }

    @DisplayName("사용자가 댓글을 작성할 수 있다.")
    @org.junit.jupiter.api.Test
    void writeComment() {

        // given
        // 1-5. 댓글 생성에 필요한 내용 준비

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

        // then
//        검증
    }

    @DisplayName("사용자가 댓글을 수정할 수 있다.")
    @org.junit.jupiter.api.Test
    void updateComment() {

        // given
        // 2-6. 댓글 생성

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

        // then
        // 검증
    }

    @DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
    @org.junit.jupiter.api.Test
    void cannotUpdateCommentWhenUserIsNotWriter() {


        // given
        // 3-3. 사용자2 생성에 필요한 내용 준비
        // 3-4. 사용자2 생성

        // 3-7. 사용자1의 댓글 생성에 필요한 내용 준비
        // 3-8. 사용자1의 댓글 생성

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

        // then
        // 검증
    }
}

댓글을 작성해보세요.


채널톡 아이콘