[test] - Mockito로 stubbing 하기

희운·2025년 12월 19일

Spring_test

목록 보기
1/1

@MockitoBean

MockitoBean 은 Application Context
이미 존재하는 빈이면 Mock 빈 으로 대체한다.
존재하지 않으면 Mock 빈 으로 새로 등록한다.

가짜 객체를 빈으로 등록하고,
그 빈이 어떤 상태를 반환할지 정하면 된다.

위와 같이 Mock 빈과 같은 Mock 객체는 언제 사용하는것일까?


@Transactional
@SpringBootTest
@ActiveProfiles(value = "test")
class OrderStatisticsServiceTest {


    @Autowired
    private OrderStatisticsService orderStatisticsService;

    @Autowired
    private OrderProductRepository orderProductRepository;

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private MailSendHistoryRepository mailSendHistoryRepository;

    @MockitoBean
    private MailSendClient mailSendClient;


    @AfterEach
    void tearDown() {
        orderProductRepository.deleteAllInBatch();
        orderRepository.deleteAllInBatch();
        productRepository.deleteAllInBatch();
        mailSendHistoryRepository.deleteAllInBatch();
    }


    @DisplayName("결제완료 주문들을 조회하여 매출 통계 메일을 전송한다.")
    @Test
    void sendOrderStatisticsMail() {
        //given
        LocalDateTime now = LocalDateTime.of(2023, 3, 5, 0, 0);
        Product product1 = createProduct(HANDMADE, "001", 1000);
        Product product2 = createProduct(HANDMADE, "002", 2000);
        Product product3 = createProduct(HANDMADE, "003", 3000);
        List<Product> products = List.of(product1, product2, product3);
        productRepository.saveAll(products);


        Order order1 = createPaymentCompletedOrder(LocalDateTime.of(2023, 3, 4, 23, 59, 59), products);
        Order order2 = createPaymentCompletedOrder(now, products);
        Order order4 = createPaymentCompletedOrder(LocalDateTime.of(2023, 3, 5, 23, 59, 59), products);
        Order order3 = createPaymentCompletedOrder(LocalDateTime.of(2023, 3, 6, 0, 0), products);

        // stubbing : 목 객체에 원하는 행위를 정의하는 것
        when(mailSendClient.sendEmail(any(String.class), any(String.class), any(String.class), any(String.class)))
                .thenReturn(true);

        // when
        boolean result = orderStatisticsService.sendOrderStatisticsMail(LocalDate.of(2023, 3, 5), "test@test.com");

        //then
        assertThat(result).isTrue();

        List<MailSendHistory> histories = mailSendHistoryRepository.findAll();
        assertThat(histories).hasSize(1)
                .extracting("content")
                .contains("총 매출 합계는 12000원입니다.");
    }

    private Order createPaymentCompletedOrder(LocalDateTime now, List<Product> products) {
        Order order = Order.builder()
                .products(products)
                .orderStatus(PAYMENT_COMPLETED)
                .registeredDateTime(now)
                .build();
        orderRepository.save(order);

        return order;
    }


    private Product createProduct(ProductType type, String productNumber, int price) {
        return Product.builder()
                .type(type)
                .productNumber(productNumber)
                .price(price)
                .sellingStatus(SELLING)
                .name("메뉴 이름")
                .build();
    }

}


테스트 클래스의 필드를 보면 @Autowired 를 통해서 의존관계 주입을 해주었다.

하지만 하나의 클래스는 @Autowired 를 사용하지 않은것을 확인할 수 있다.

    @MockitoBean
    private MailSendClient mailSendClient;

이건 왜 @Autowired 를 통해서 DI 를 하는것이 아닌 @MockitoBean 으로 가짜 빈을 Application Context 에 등록하는것일까

public boolean sendOrderStatisticsMail(LocalDate orderDate, String email) {

        // 해당 일자에 결제완료된 주문들을 가져와서
        List<Order> orders = orderRepository.findOrdersBy(
                orderDate.atStartOfDay(),
                orderDate.plusDays(1).atStartOfDay(),
                PAYMENT_COMPLETED
        );

        // 총 매출 합계를 계산하고

        int totalPrice = orders.stream()
                .mapToInt(order -> order.getTotalPrice())
                .sum();

        // 메일 전송
        boolean result = mailService.sendMail(
                "no-reply@cafekiosk.com",
                email,
                String.format("[매출통계] %s", orderDate),
                String.format("총 매출 합계는 %s원입니다.", totalPrice)
        );

        if (!result) {
            throw new IllegalArgumentException("매출 통계 메일 전송에 실패했습니다.");
        }

        return true;
    }

sendEmailService 내부에서 메일 전송을 한다.


package sample.cafekiosk.spring.api.service.mail;


import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import sample.cafekiosk.spring.client.mail.MailSendClient;
import sample.cafekiosk.spring.domain.history.MailSendHistory;
import sample.cafekiosk.spring.domain.history.MailSendHistoryRepository;

@RequiredArgsConstructor
@Service
public class MailService {

    private final MailSendClient mailSendClient;
    private final MailSendHistoryRepository mailSendHistoryRepository;


    public boolean sendMail(String fromMail, String toEmail, String subject, String content) {

        boolean result = mailSendClient.sendEmail(fromMail, toEmail, subject, content);

        if (result) {
            mailSendHistoryRepository.save(MailSendHistory.builder()
                    .fromEmail(fromMail)
                    .toEmail(toEmail)
                    .subject(subject)
                    .content(content)
                    .build()
            );
            return true;
        }

        return false;
    }
}

여기서 실제로 메일을 전송하는 객체는 MailSendClient 가 담당한다.

그럼 MailSendClient 에서 외부 네트워크를 사용해서 매번 테스트를 진행하는건 매우 비효율적일것이다. 테스트를 할때마다 실제로 메일을 전송하는건 바람직하지 않다.

이런 경우에 Mock 객체를 사용해서 메일 전송 로직을 작동한다고 가정하고 테스트를 진행할 수 있다.

테스트 코드에서는 실제 객체가 아닌 가짜 객체를 등록해 사용하기 위해서

@MockitoBean private MailSendClient mailSendClient;

으로 Mock 빈으로 등록하는것이다.

Mock 빈을 이용해서 반환하도록 하는것을 Stubbing 이라고 한다.


// stubbing : 목 객체에 원하는 행위를 정의하는 것
 when(mailSendClient.sendEmail(any(String.class), any(String.class), any(String.class), any(String.class)))
                .thenReturn(true);

원하는 행위를 정의하고 사용하면 된다.

참고로 위와 같은 과정은 Given 에서 진행하는것이 맞다.

profile
기록하는 공간

0개의 댓글