단위테스트의 필요성함수 호출 횟수 검증 ( verify() )inOrderMock을 만들기 위해 사용하는 annotationSpring Boot에서 지원해주는 어노테이션BDD MockitoMockito와 BDDMockito는 뭐가 다를까?Mockito willReturn(Object, Object[]…)BDDMockito void method함수호출횟수 검증ArgumentCaptor , ArgumentMatchersCustomArgumentMatcher 🆚 ArgumentCaptor
- Mockito 공식 사이트 — mocking frameworks for unit test in java
- [ Javadoc ] Mockito features
단위테스트의 필요성
- Spring은 DI(Dependency Injection)를 지원해주는데, 이는 객체 간의 의존성을 Spring이 관리해주는 것임. 덕분에 개발자는 의존성 주입을 신경쓰지 않고 객체 간의 의존관계만 잘 고민해서 객체를 설계하면 된다
- 그러나 테스트를 할 때는 단위 테스트를 하고 싶어도 의존성을 가지는 다른 객체에 의해 테스트 결과가 영향을 받을 수 있기에, 의존을 가지는 객체를 우리가 원하는 동작만 하도록 만드는 것이 Mock 객체임
함수 호출 횟수 검증 ( verify()
)
[ Velog ] Verify Method Calls
[ Baeldung ] Mockito Verify Cookbook
- 해당 테스트 안에서 특정 메소드를 호출 했는지에 대한 검증
//using mock mockedList.add("once"); mockedList.add("twice"); mockedList.add("twice"); mockedList.add("three times"); mockedList.add("three times"); mockedList.add("three times"); //following two verifications work exactly the same - times(1) is used by default verify(mockedList).add("once"); verify(mockedList, times(1)).add("once"); //exact number of invocations verification verify(mockedList, times(2)).add("twice"); verify(mockedList, times(3)).add("three times"); //verification using never(). never() is an alias to times(0) verify(mockedList, never()).add("never happened"); //verification using atLeast()/atMost() verify(mockedList, atMostOnce()).add("once"); verify(mockedList, atLeastOnce()).add("three times"); verify(mockedList, atLeast(2)).add("three times"); verify(mockedList, atMost(5)).add("three times");
inOrder
- verify 를 호출 순서대로 호출했는지를 검증하고자 할 때 사용함
// stub를 활용한 Mocking. 상태 검증 class OrderRepositoryStub implements OrderRepository{ @Override public Order insert(Order order) { return null; } } @Test @DisplayName("오더가 생성돼야 한다. (stub)") void createOrder() { // Given MemoryVoucherRepository voucherRepository = new MemoryVoucherRepository(); Voucher fixedAmountVoucher = new FixedAmountVoucher(UUID.randomUUID(), 100L); voucherRepository.insert(fixedAmountVoucher); var sut = new OrderService(new VoucherService(voucherRepository), new OrderRepositoryStub()); // When var order = sut.createOrder(UUID.randomUUID(), List.of(new OrderItem(UUID.randomUUID(), 200, 1)), fixedAmountVoucher.getVoucherId()); // Then assertThat(order.totalAmount(), is(100L)); assertThat(order.getVoucher().isEmpty(), is(false)); assertThat(order.getVoucher().get().getVoucherId(), is(fixedAmountVoucher.getVoucherId())); assertThat(order.getOrderStatus(), is(OrderStatus.ACCEPTED)); } // Mock 객체를 활용한 모킹. 행위 검증 @Test @DisplayName("오더가 생성돼야 한다. (mock)") void createOrderByMock() { // Given var voucherServiceMock = Mockito.mock(VoucherService.class); Voucher fixedAmountVoucher = new FixedAmountVoucher(UUID.randomUUID(), 100L); var orderRepositoryMock = Mockito.mock(OrderRepository.class); Mockito.when(voucherServiceMock.getVoucher(fixedAmountVoucher.getVoucherId())).thenReturn(fixedAmountVoucher); var sut = new OrderService(voucherServiceMock, orderRepositoryMock); // 이렇게 정의한 부분만 리턴한 대로 값을 반환함. 정의 안한 부분에 대한 호출은 에러 발생 // When var order = sut.createOrder( UUID.randomUUID(), List.of(new OrderItem(UUID.randomUUID(), 200, 1)), fixedAmountVoucher.getVoucherId()); // Then assertThat(order.totalAmount(), is(100L)); assertThat(order.getVoucher().isEmpty(), is(false)); var inOrder = Mockito.inOrder(voucherServiceMock, orderRepositoryMock); // mockito 객체들의 호출 순서를 보장하는 방법(inOrder) inOrder.verify(voucherServiceMock).getVoucher(fixedAmountVoucher.getVoucherId()); // 해당 메서드가 호출이 됐는지를 검증 inOrder.verify(orderRepositoryMock).insert(order); inOrder.verify(voucherServiceMock).useVoucher(fixedAmountVoucher); }
Mock을 만들기 위해 사용하는 annotation
@Mock
: 해당 클래스를 mock implementation으로 만듦
@InjectMocks
: @Mock으로 명시된 mock을 주입하여 클래스의 인스턴스를 만듦
해당 Annotation들이 동작하게 하려면, @ExtendWith(MockitoExtension.class)
를 사용하거나, MockitoAnnotations.openMocks(this)
를 적용해야 함 (근데 이렇게 Annotation 기반으로 Mock 주입시, 실제 BDDMocktio.mock 으로 내가 직접 만들어서(43ms
) 넣어주는거에 비해 7배(283ms
)나 느림
@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; private final User user = new User("test-user@gmail.com", "abce12!@", Role.ROLE_USER); }
class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; private final User user = new User("test-user@gmail.com", "abce12!@", Role.ROLE_USER); private AutoCloseable closeable; @BeforeEach public void openMocks() { closeable = MockitoAnnotations.openMocks(this); } @AfterEach public void releaseMocks() throws Exception { closeable.close(); }
Spring Boot에서 지원해주는 어노테이션
@MockBean
: ApplicationContext
안에 있는 bean 의 Mockito mock을 정의하기 위해 사용 (해당 Mock이 ApplicationContext에 존재하는 Bean이라면 @MockBean
아니라면 @Mock
사용)
@SpringBootTest class MyTests { @Autowired private Reverser reverser; @MockBean private RemoteService remoteService; @Test void exampleTest() { given(this.remoteService.getValue()).willReturn("spring"); String reverse = this.reverser.getReverseValue(); // Calls injected RemoteService assertThat(reverse).isEqualTo("gnirps"); } }
BDD Mockito
[ Javadoc ] BDD Mockito
[ Baeldung ] Mocking void methods with Mockito
BDDMockito provides BDD aliases for various Mockito methods, so we can write our Arrange step using given (instead of when), likewise, we could write our Assert step using then (instead of verify) - Quick Guide to BDDMockito(Baeldung)
Mockito와 BDDMockito는 뭐가 다를까?
when(phoneBookRepository.contains(momContactName)) .thenReturn(false); phoneBookService.register(momContactName, momPhoneNumber); verify(phoneBookRepository) .insert(momContactName, momPhoneNumber);
- when()
- thenReturn()
- verify()
⇒ 개념적으로 given 에 해당하는 부분이 when 이어서 헷갈리기 쉬움
public interface MemberRepository extends JpaRepository<Member, Long> { boolean existsByEmail(String email); } public class MemberService { @Autowired private MemberRepository memberRepository; public boolean isExistEmail(String email) { return memberRepository.existsByEmail(email); // 존재하면 true, 존재하지 않으면 false 반환 } } public class MemberServiceTest { @MockBean private MemberService memberService; private final MemberRepository memberRepository = mock(MemberRepository.class); @BeforeEach void stubbing() { given(memberRepository.existsByEmail(any(String.class))) .willReturn(false); } }
Mockito willReturn(Object, Object[]…)
동일한 argument로 method호출 시, 다른 결과를 내뱉고 싶을때는 아래와 같이 하면 됨
given(roomInstanceService.getByRoomId(lobbyRoomId, organization.getId())) .willReturn(Collections.emptyList(), List.of(lobbyInstance));
- 위와 같이 구성하면 처음에 메서드 호출시에는 빈리스트를, 두번째 호출시에는
List.of(lobbyInstance)
를 반환함
BDDMockito void method
// void method throw exception willThrow(new EmailSendException("이메일 전송 실패")).given(emailSender) .sendMimeMessage(anyString(), anyString(), anyString());
함수호출횟수 검증
then(adminRepository).should(times(1)).saveAll(argumentCaptor.capture()); then(adminRepository).should(times(1)).save(any(Admin.class)); then(gameServerService).should(times(0)).createRoomInstance(anyInt(), anyString(), anyString(), anyInt(), anyString());
ArgumentCaptor , ArgumentMatchers
[ Baeldung ] Mockito Argument Matcher, Mockito Argument Captor
ArgumentCaptor
: method에 넘겨지는 인자를 잡아낼 수 있음. 우리가 테스트하고자 하는 메서드 안에서 넘겨지는 인자에 접근하지 못할 때 유용하게 사용이 가능함
@Test void test() { ArgumentCaptor<List<Admin>> argumentCaptor = ArgumentCaptor.forClass(List.class); verify(adminRepository, times(1)).saveAll(argumentCaptor.capture()); List<Admin> inputAdmins = argumentCaptor.getValue(); assertThat(inputAdmins).hasSize(3); assertThat(inputAdmins).extracting(Admin::getEmail) .containsExactlyInAnyOrder(admin1.getEmail(), admin2.getEmail(), admin3.getEmail()); assertThat(inputAdmins).extracting(Admin::getOrganization) .containsExactlyInAnyOrder(admin1.getOrganization(), admin2.getOrganization(), admin3.getOrganization()); }
ArgumentCaptor를 사용할 때는 stubbing으로 사용하지말라. 테스트 가독성을 떨어뜨린다 (당연한 말인것 같음)
Credentials credentials = new Credentials("baeldung", "correct_password", "correct_key"); when(platform.authenticate(eq(credentials))).thenReturn(AuthenticationStatus.AUTHENTICATED); assertTrue(emailService.authenticatedSuccessfully(credentials)); // stubbing으로 사용한 경우. 이런짓을 왜하지..? Credentials credentials = new Credentials("baeldung", "correct_password", "correct_key"); when(platform.authenticate(credentialsCaptor.capture())).thenReturn(AuthenticationStatus.AUTHENTICATED); assertTrue(emailService.authenticatedSuccessfully(credentials)); assertEquals(credentials, credentialsCaptor.getValue());
ArgumentMatcher
: 메서드에 어떤 인자가 넘겨지는지를 좀 더 넓게 명시하거나 모르는 값에 대해서 반응하도록 Mock을 구성하고 싶을 때 사용함
doReturn("Flower").when(flowerService).analyze("poppy"); when(flowerService.analyze(anyString())).thenReturn("Flower");
- ArgumentMatcher를 이용할 때는 메서드의 모든 인자에 대해 적용하든지, 아니면 아예 적용을 하지 않아야 함
CustomArgumentMatcher
MessageDTO messageDTO = new MessageDTO(); messageDTO.setFrom("me"); messageDTO.setTo("you"); messageDTO.setText("Hello, you!"); messageController.createMessage(messageDTO); verify(messageService, times(1)).deliverMessage(any(Message.class));
public class MessageMatcher implements ArgumentMatcher<Message> { private Message left; // constructors @Override public boolean matches(Message right) { return left.getFrom().equals(right.getFrom()) && left.getTo().equals(right.getTo()) && left.getText().equals(right.getText()) && right.getDate() != null && right.getId() != null; } }
MessageDTO messageDTO = new MessageDTO(); messageDTO.setFrom("me"); messageDTO.setTo("you"); messageDTO.setText("Hello, you!"); messageController.createMessage(messageDTO); Message message = new Message(); message.setFrom("me"); message.setTo("you"); message.setText("Hello, you!"); verify(messageService, times(1)).deliverMessage(argThat(new MessageMatcher(message)));
CustomArgumentMatcher 🆚 ArgumentCaptor
ArgumentCaptor
를 쓸 때는 assert를 하고자 할 때. 해당 메서드에 넘겨진 인자에 대해서 assertion을 하고자 할 때 사용하기 적절함
CustomArgumentMatcher
는 stubbing으로 사용하고자 할 때 좋다. 위에서 ArgumentCaptor를 stubbing에 쓰지 말라고 한 것에 반해,ArgumentMatcher
는 stubbing에 좋음