단위 테스트에서 @InjectMocks, @Mock VS @MockBean 이해하기

kindof

·

2023. 4. 15. 22:51

1. @SpringBootTest와 @MockBean

@SpringBootTest는 Junit 테스트에서 @SpringBootApplication 어노테이션을 찾아 하위의 모든 Bean들을 스캔하여 Spring ApplicationContext를 로딩하는 데 사용됩니다.

 

그리고 이렇게 로딩된 컨텍스트 아래에서 @Autowired로 간단하게 테스트의 대상이 되는 객체를 주입할 수 있게 됩니다.

 

한편, @MockBean 역시 SpringBoot에서 제공하는 어노테이션으로, Spring ApplicationContext에서 관리되는 Bean을 대체하기 위해 Mockito와 함께 사용됩니다.

 

즉, @SpringBootTest를 통해 로드한 빈을 Mock 객체로 주입할 수 있게 만드는 것이죠.

 

그러면 지금까지 설명한 @SpringBootTest, @MockBean 어노테이션을 통해 아래와 같이 Mock 기반 테스트 코드를 작성할 수 있습니다.

 

먼저 테스트 코드를 작성하기 위한 준비 과정으로 간단한 서비스 코드를 작성하겠습니다.

 

[BookService.java]

@Service
@RequiredArgsConstructor
@Slf4j
public class BookService {
    private final BookRepository bookRepository;
    private final DeliveryService deliveryService;

    public Book purchase(String title) {
        Book book = bookRepository.findByTitle(title);

        if (book == null) {
            log.warn("Purchase Book title : {} does not exist", title);
            throw new EntityNotFoundException("해당 도서는 존재하지 않습니다.");
        }

        if (book.getStock() <= 0) {
            log.warn("Purchase Book title : {} has no stock", title);
            throw new IllegalStateException("해당 도서의 재고가 부족합니다.");
        }

        // 비즈니스 로직
        // ...
        // 재고를 조정한다.
        updateStock(book);

        deliveryService.deliver();

        return book;
    }
    
    ...
}

bookRepository에서 책의 이름을 통해 책을 조회하고 책이 존재하지 않으면 EntityNotFoundException, 책의 재고가 부족하면 IllegalStateException을 일으킵니다.

 

이 상황에서 '책의 재고가 부족할 때 적절한 예외가 발생하는지'를 테스트하기 위해 아래와 같이 테스트 코드를 작성할 수 있습니다.

테스트 코드

bookRepository를 @MockBean으로 등록하여 실제 BookService 내에서 동작하는 BookRepository의 동작을 제어하고 있습니다.

 

즉, 위 테스트 코드에서는 어떤 책의 제목으로 조회하더라도 재고가 0인 상태의 책을 리턴하여 IllegalStateException이 발생하는 것을 테스트할 수 있죠.

 

2. @SpringBootTest는 단위 테스트에서 사용하기엔 너무 무겁다.

하지만 위와 같은 단위 테스트를 위해 @SpringBootTest를 사용하여 모든 Bean들을 불러오게 되면 테스트 클래스마다 로딩 시간이 쌓이게 되 테스트 자체는 몇 초로 끝나지만 테스트 환경을 구성하는 데 수 분이 넘게 소요되는 결과를 낳게 됩니다.

 

어플리케이션의 규모가 커지면 관리하는 Bean만 수 백개가 넘기 때문이죠.

 

그렇다면 @SpringBootTest 없이 Mock 기반 단위 테스트를 어떻게 작성할 수 있을까요?

 

먼저, 단순히 위 테스트 코드에서 @SpringBootTest 을 사용하지 않고 @ExtendWith(MockitoExtension.class)를 사용해보면 아래와 같이 this.bookRepository에 대한 NullPointerException이 터지게 됩니다.

테스트 코드가 동작하지 않는다.

이유는 @ExtendWith(MockitoExtension.class) 어노테이션이 사용되어 테스트 클래스에서 Mockito를 사용하도록 지정되었지만, 이 클래스에는 Spring ApplicationContext가 로드되지 않아 @Autowired로 BookService를 주입할 수 없는 것이죠.

 

3. @InjectMocks + @Mock

따라서 위와 같은 문제를 해결하기 위해 @InjectMocks + @Mock 어노테이션을 사용할 수 있습니다.

테스트 성공

@InjectMocks는 Mockito 프레임워크에서 사용되는 어노테이션으로, 목 객체를 생성하고 주입하는 데 사용됩니다. 

 

위 테스트 코드에서는 @ExtendWith(MockitoExtension.class) 어노테이션을 클래스에 추가해서 Mockito를 활성화하고, @InjectMocks 어노테이션을 BookService에 추가하여 목 객체의 의존성을 주입하는 것이죠.

 

그리고 BookRepository에는 @Mock 어노테이션을 추가해서 BookService에 대한 의존성을 실제 BookRepository가 아닌, Mock BookRepository로 주입한 것입니다. 그래서 우리가 원하는대로 테스트 코드가 성공할 수 있게 됩니다.

 

 

4. @Mock vs @MockBean

@Mock 어노테이션과 @MockBean 어노테이션은 서로 다른 목 객체를 생성합니다.

 

@Mock 어노테이션은 Mockito에서 제공하는 목 객체를 생성하는 데 반해 @MockBean 어노테이션은 스프링에서 제공하는 목 객체를 생성합니다.

 

따라서, @MockBean 어노테이션을 사용해서 목 객체를 생성하려면 스프링 Context가 필요하고 이를 위해 맨 처음에 말한 @SpringBootTest 등의 컨텍스트 로딩을 위한 어노테이션이 필요합니다.

 

그래서 위의 테스트 코드에서 @Mock 어노테이션을 @MockBean으로 대체하게 되면 아래와 같이 BookRepository에 대한 NullPointerException이 다시 발생하게 됩니다.

@MockBean 실패

 

5. 정리

이번 포스팅에서는 "단위 테스트에서 Mock 객체를 사용할 때 어떻게 해야 할까?"에 대해 정리해봤는데요.

 

@SpringBootTest는 통합 테스트에 어울리는 무거운 어노테이션이기 때문에 이를 사용하기 보다는, Mockito 라이브러리의 적절한 어노테이션을 사용해 단위 테스트를 작성해야 한다는 것이 중요한 포인트였습니다.

 

그리고 특히, 비슷해 보이기 때문에 헷갈릴 수 있는 @Mock, @InjectMocks, @MockBean 등의 개념과 차이점에 대해서도 잘 알아두어야 합니다.

 

Mocking을 하는 방식에 있어서 오늘 소개한 어노테이션 이외에 여러 가지 편의를 제공하는 어노테이션들도 있고 이를 활용하는 방법에 대해서는 이후에 또 정리해보겠습니다.

 

감사합니다.