Stubbing
데이터를 메일로 전송해주는 기능을 Mocking 테스트를 해보자!
왜냐면 메일 mailSend 같은 테스트는 매번 외부로 보내서 테스트 해야하는데, 이런 경우 테스트가 힘들 수 있다.
따라서, mailSendClient 컴포넌트의 sendEmail를 Mocking 처리해보자
이걸 Mocking 으로 Stubbing 한다고 말한다.
@MockBean
private MailSendClient mailSendClient;
@DisplayName("결제완료 주문들을 조회하여 매출 통계 메일을 전송한다.")
@Test
void sendOrderStatisticsMail() {
// given
//products 생성 및 저장
.. 생략
productRepository.saveAll(products);
//생성한 products로 order 생성
.. 생략
// 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원입니다.");
}
- mailSendClient 컴포넌트의 sendEmail을 stubbing
- sendEmail에 String 매개변수가 4개가 들어온다면 thenReturn true를 반환한다.
- 메일이 정상적으로 발송된다면 mailSendHistoryRepository에서 save 되기 때문에 histories 를 테스트 할 수 있게된다.
- MailSendClient 을 @MockBean 로 띄워준다.
Test Double
- Dummy : 아무것도 하지 않는 깡통 객체
- Fack : 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기는 부족한 객체
- ex : FackRepository
- Stub : 테스트에서 요청한 것에 대해서 미리 준비한 결과를 제공하는 객체
- 그 외에는 응답하지 않는다.
- Spy : Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체
- 일부는 실제 객체처럼 동작시키고 일부만 Stubbing 할 수 있다.
- Mock : 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체
** Stub 과 Mock 이 좀 헷갈리는 면이 있다.
- 가짜 객체이고, 이런 리턴을 하는 것은 동일하지만 검증하려는 목적이 다르다.
- Stub는 어떠한 메소드의 이런 기능을 했을 때 상태가 이렇게 바뀌었다. 상태를 get 해서 검증한다. (상태 검증)
- ex) assertEquals(20, result); stub이 올바른 값을 반환했는데 상태 확인
- Mock은 when 해서 어떠한 메서드를 값을 넘겨줬을 때 어떤 값으로 리턴한다는 걸 검증한다.(행위 검증)
- verify(mock, times(1)).calculate(anyInt()); 메서드 호출 횟수 검증
순수 Mockito 로 검증해보기
@MockBean
- @SpringBootTest가 떠야함.
그렇다면 SpringBoostTest가 뜨지 않는 단위 테스트는?
순수 Mockito 로 단위 테스트 해보기
@ExtendWith(MockitoExtension.class)
class MailServiceTest {
@DisplayName("메일 전송 테스트")
@Test
void test() {
// given
MailSendClient mailSendClient = Mockito.mock(MailSendClient.class);
MailSendHistoryRepository mailSendHistoryRepository = Mockito.mock(MailSendHistoryRepository.class);
MailService mailService = new MailService(mailSendClient, mailSendHistoryRepository);
when(mailSendClient.sendMail(anyString(),anyString(),anyString(),anyString()))
.thenReturn(true);
// when
boolean result = mailService.sendMail("", "", "", "");
// then
assertThat(result).isTrue();
verify(mailSendHistoryRepository,times(1)).save(any(MailSendHistory.class));
}
}
- Mockito.mock 객체를 만들어서 테스트
- @ExtendWith(MockitoExtension.class) 통해 Mock을 통해 클래스를 만든다고 알려준다.
- 그럼sendMail 안의 mailSendHistoryRepository.save()는? -> Mock은 가짜 객체라 save 되지 않음. null 기본 값을 반환하여 해결
- 좀 더 명확하게 테스트하고 싶은 경우 verify사용
- MailSendClient가 class가 호출 되었을 때 mailSendHistoryRepository 가 한번 save 되었는지 확인
@InjectMocks
또한 내부에서 선언하는게 아닌 어노테이션을 사용할 수 있다.
@ExtendWith(MockitoExtension.class)
class MailServiceTest {
@Mock
private MailSendClient mailSendClient;
@Mock
private MailSendHistoryRepository mailSendHistoryRepository;
@InjectMocks
private MailService mailService;
@DisplayName("메일 전송 테스트")
@Test
void sendMail() {
// given
when(mailSendClient.sendMail(anyString(),anyString(),anyString(),anyString()))
.thenReturn(true);
// when // then 생략
}
}
@Spy
MailSendClient 의 일부만 Mock 처리하고 나머지는 실제 호출을 하고 싶다면?
@Slf4j
@Component
public class MailSendClient {
public boolean sendEmail(String fromEmail, String toEmail, String subject, String content) {
log.info("메일 전송");
throw new IllegalArgumentException("메일 전송");
}
public void a() {
log.info("a");
}
public void b() {
log.info("b");
}
public void c() {
log.info("c");
}
}
@ExtendWith(MockitoExtension.class)
class MailServiceTest {
@Spy
private MailSendClient mailSendClient;
@Mock
private MailSendHistoryRepository mailSendHistoryRepository;
@InjectMocks
private MailService mailService;
@DisplayName("메일 전송 테스트")
@Test
void sendMail() {
// given
doReturn(true).when(mailSendClient)
.sendMail(anyString(),anyString(),anyString(),anyString());
// when // then 생략
}
}
- @spy로 만들어진 MailSendClient 객체의 실제로직이 실행된다.
- 다만 when으로 만들어진 가짜객체는 실제로직이 아닌, 모킹된다.
BDDMockito
mockito의 경우 when으로 테스트 대상이 true를 반환한다고 출발한다. (given)
→ given 절인데 when 을 사용하네?
Mockito.when(mailSendClient.sendMail(anyString(),anyString(),anyString(),anyString()))
.thenReturn(true);
BDDMockito.given(mailSendClient.sendMail(anyString(),anyString(),anyString(),anyString()))
.willReturn(true);
BDD 스타일이 맞춰서 작성할 수 있게 끔 BDDMockito 를 사용할 수 있다.
언제 Mocking을 써야할까?
물론 mocking 할 수 있는 모든 객체를 Mocking해버리면 테스트는 간편해질 것이다.
다만 서로 연관되어 발생하는 객체들을 어떻게 Mocking으로 보장할 수 있는가에 대한 문제가 남는다.
그렇기 때문에 최대한 객체로 테스트하면서, 컨트롤 할 수 없는 외부시스템을 Mocking 처리하여 테스트하는 방법을 추천한다.
출처 ; https://www.inflearn.com/course/practical-testing-실용적인-테스트-가이드
'TDD' 카테고리의 다른 글
값이나 환경을 바꿔가며 테스트 하고 싶을 때 (0) | 2024.12.08 |
---|---|
더 나은 테스트를 작성하기 위한 고민 (0) | 2024.12.01 |
응답 예외처리와 책임 분리 (0) | 2024.11.25 |
Presentation Layer의 테스트 (0) | 2024.11.24 |
레이어드 아키텍처(Layered Architecture) 와 테스트 (0) | 2024.11.18 |