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 에서 진행하는것이 맞다.