이 글은 온라인 강의를 듣고 해당 내용을 직접 실습해보며 정리한 글입니다. 더 자세한 내용은 아래 강의에서 확인할 수 있습니다.
더 자바, 애플리케이션을 테스트하는 다양한 방법 - 인프런 | 강의
✔Mockito: Mock 을 지원하는 프레임워크
✔Mock: 진짜 객체와 비슷하게 동작하지만 우리가 컨트롤할 수 있는 객체
- 어떻게 활용: 외부 API, DB를 사용하는 경우 DAO, repository 객체를 mock 으로 만들고 mockito 를 사용해 동작 방식을 코딩 하고 테스트를 한다.
- 장점: 외부 환경으로부터 독립적으로 테스트 가능
- 테스트에 대한 의견: 내가 이미 구현한 클래스는 mocking 할 필요는 없다. 하지만 외부 서비스는 mocking 을 하는 게 좋겠다.
- Mocking 하기 좋은 경우: 인터페이스를 사용해 개발하는 경우. 구현체를 mocking하면 좋다.
- Stubbing: Mock 객체의 행동을 조작하는 것
의존성 주입하기
- spring-boot-starter-test 가 mockito 를 알아서 주입해준다.
- 스프링부트가 아니면, 다음을 추가한다.
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
Mock 객체 생성하는 법
준비
StudyService는 생성자로 인터페이스를 받는다.
public class StudyService {
private final MemberService memberService;
private final StudyRepository repository;
public StudyService(MemberService memberService, StudyRepository repository) {
...
}
}
StudyService를 테스트 하기 위해서는 MemberService, StudyRepository 인터페이스를 Mock 객체로 생성해야 한다.
① Mock 객체 생성: 메소드로
@Test
void createStudyService() {
MemberService memberService = Mockito.mock(MemberService.class);
StudyRepository studyRepository = Mockito.mock(StudyRepository.class);
StudyService studyService = new StudyService(memberService, studyRepository);
assertNotNull(studyService);
}
MemberService, StudyRepository 인터페이스를 직접 구현하는 일을 mockito.mock 이 대신 해준다.
② Mock 객체 생성: 인스턴스 변수로
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Mock
MemberService memberService;
@Mock
StudyRepository studyRepository;
@Test
void createStudyService() {
StudyService studyService = new StudyService(memberService, studyRepository);
assertNotNull(studyService);
}
}
@Mock
: Mockito.mock(MemberService.class) 을 간단하게 애노테이션으로 만들 수 있다.@ExtendWith(MockitoExtension.class)
: @Mock 애노테이션을 처리해주는 확장 모델이다.
③ Mock 객체 생성: 메소드 파라미터로
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Test
void createStudyService(@Mock MemberService memberService, @Mock StudyRepository studyRepository) {
StudyService studyService = new StudyService(memberService, studyRepository);
assertNotNull(studyService);
}
}
• @ExtendWith(MockitoExtension.class)
는 붙여줘야한다.
Stubbing 하는 법
기본적으로 모든 Mock 객체는
- 리턴 타입이 객체 이면, null 을 리턴한다.
- 리턴 타입이 Optional 이면, Optional.empty() 를 리턴한다.
- 리턴 타입이 primitive 타입이면, 기본 Primitive 값 을 리턴한다. (int 는 0, boolean은 false)
- 리턴 타입이 콜렉션 이면, 비어있는 콜렉션 을 리턴한다.
- 리턴 타입이 void 면, 예외를 던지지 않고 아무런 일도 발생하지 않는다.
이런 특성을 가진 Mock 객체를 우리가 원하는 것을 리턴하도록 조작하는 것을 Stubbing 이라고 한다.
① return 있는 메소드 stubbing 하기
when([리턴있는메소드]).thenReturn([리턴값])
when([리턴있는메소드]).thenThrow([익셉션])
@Test
@DisplayName("return 있는 메소드 stubbing 하기")
void stubReturnMethod(@Mock MemberService memberService) {
String email = "catsarah3333@gmail.com";
Member member = new Member(1L, email);
// stubbing
when(memberService.findById(1L)).thenReturn(Optional.of(member)); // memberId가 1L 이어야지만 member 를 반환한다.
when(memberService.findById(any())).thenReturn(Optional.of(member)); // ArgumentMatchers.any(): memberId가 뭐든간에 member 를 반환한다.
when(memberService.findById(-1L)).thenThrow(new RuntimeException()); // 인자가 -1L이면 RuntimeException을 던져라.
assertEquals(email, memberService.findById(1L).get().getEmail());
}
any()
: 인자를 다양하게 주도록 도와주는 메소드. ArgumentMatchers 를 참고한다. https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#3
② return 없는 메소드 예외 발생 stubbing 하기
doThrow([익셉션]).when([클래스]).[리턴없는메소드명]
@Test
@DisplayName("return 없는 메소드 stubbing 하기")
void stubVoidMethod(@Mock MemberService memberService) {
// stubbing
// void 메소드에서 예외 던지기
doThrow(new RuntimeException()).when(memberService).validate(-10L);
assertThrows(RuntimeException.class, () -> memberService.validate(-10L));
}
③ 동일한 파라미터로 여러 번 호출될 때마다 각기 다르게 return 하도록 stubbing 하기
@Test
@DisplayName("동일한 파라미터로 여러 번 호출될 때마다 각기 다르게 return 하도록 stubbing 하기")
void stubDifferentReturnMethod(@Mock MemberService memberService) {
String email = "catsarah3333@gmail.com";
Member member = new Member(1L, email);
// stubbing
when(memberService.findById(any()))
.thenReturn(Optional.of(member)) // findById() 1번째 호출: Optional<Member> 반환
.thenThrow(RuntimeException.class) // findById() 2번째 호출: RuntimeException 던짐
.thenReturn(Optional.empty()) // findById() 3번째 호출: Optional.empty() 반환
;
assertEquals(email, memberService.findById(1L).get().getEmail());
assertThrows(RuntimeException.class, () -> memberService.findById(3L));
assertEquals(Optional.empty(), memberService.findById(10L));
}
• thenReturn(), thenThrow() 를 메소드 체이닝 해준다.
Mock 메소드 호출 검증 하는 법 (verifying)
① 메소드가 몇 번 호출 됐는지 검증
verify([mock 객체], [호출 돼야 하는 횟수]).[mock 객체 메소드]
@Test
@DisplayName("메소드 호출 횟수 검증")
public void verifyCallCnt(@Mock MemberService memberService) {
Study study = new Study(10, "테스트");
verify(memberService, never()).notify(study); // memberService.notify() 한 번도 호출 안 돼야함
memberService.notify(study);
verify(memberService, times(1)).notify(study); // memberService.notify() 한 번 호출 돼야함
}
- notify(): 내가 만든 메소드명이다.
② 메소드 호출 순서 검증
InOrder inOrder = inOrder([mock객체])
inOrder.verify([mock객체]).[메소드명]
@Test
@DisplayName("메소드 호출 순서 검증")
public void verifyCallOrder(@Mock MemberService memberService) {
// given
Study study = new Study(10, "테스트");
Member member = new Member(1L, "catsarah3333@gmail.com");
// when
memberService.notify(study);
memberService.notify(member);
// then
InOrder inOrder = inOrder(memberService);
inOrder.verify(memberService).notify(study); // memberService.notify(study) 호출 다음에
inOrder.verify(memberService).notify(member); // memberService.notify(member) 호출 돼야함
}
③ 더 이상 검증할 호출 메소드가 없는지 체크
verifyNoMoreInteractions([mock객체])
테스트 성공인 경우
@Test
@DisplayName("성공 - 더 이상 검증할 메소드가 없는지 체크")
public void verifyNoMoreCall(@Mock MemberService memberService) {
Study study = new Study(10, "테스트");
memberService.notify(study);
verify(memberService, times(1)).notify(study);
verifyNoMoreInteractions(memberService);
}
memberService.notify(study) 가 호출되었고 이를 verify() 로 검증했으니 더 이상 검증할 memberService 호출 메소드가 없으므로 테스트 성공
테스트 실패인 경우
@Test
@DisplayName("실패 - 검증할 메소드 존재")
public void verifyNoMoreCall(@Mock MemberService memberService) {
Study study = new Study(10, "테스트");
memberService.notify(study);
// verify(memberService, times(1)).notify(study);
verifyNoMoreInteractions(memberService);
}
memberService.notify(study) 를 호출 했으나 verify() 로 검증 안 됐으므로 테스트 실패. = verify 할 interaction 이 존재하므로 실패
BDD 스타일로 Mockito 코드 변경하는 법
BDD(Behavior Driven Development)
- 메소드가 어떻게 행동해야하는지를 중심으로 작성하는 기법
- 인자 테스트 코드 작성시 기준: Given / When / Then
BDDMockito.given([stubbing할메소드]).willReturn([리턴값])
BDDMockito.then([mock객체]).should([호출횟수]).[메소드명]
BDDMockito.then([mock객체]).shouldHaveNoMoreInteractions()
Mockito 코드를 Given / When / Then 스타일에 맞춰 작성할 수 있다.
@Test
@DisplayName("mockito 를 BDD style로 변경하기")
public void bddMockito(@Mock MemberService memberService, @Mock StudyRepository studyRepository) {
// given
Study study = new Study(10, "테스트");
Member member = new Member(1L, "gmail.com");
BDDMockito.given(memberService.findById(1L)).willReturn(Optional.of(member));
// = Mockito.when(memberService.findById(1L)).thenReturn(Optional.of(member));
BDDMockito.given(studyRepository.save(study)).willReturn(study);
// = Mockito.when(studyRepository.save(study)).thenReturn(study);
// when
memberService.notify(study);
// then
BDDMockito.then(memberService).should(times(1)).notify(study);
// = Mockito.verify(memberService, times(1)).notify(study);
BDDMockito.then(memberService).shouldHaveNoMoreInteractions();
// = Mockito.verifyNoMoreInteractions(memberService);
}
기존에 작성했던 stubbing 의 Mockito.when()
절은 BDD 에선 given 에 해당하므로 명세가 어울리지 않는다. 이 때 BDDMockito
객체를 사용해 given 절로 변환한다.
Mockito 설명
글 읽어주셔서 언제나 감사합니다. 좋은 피드백, 개선 피드백 너무나도 환영합니다.
연관글
'SearchDeveloper > 애플리케이션테스트' 카테고리의 다른 글
TestContainers 설명 (0) | 2022.08.02 |
---|---|
[Postman] Runner 와 newman 으로 response 파일에 쓰기 (0) | 2022.07.24 |
Junit5 애노테이션 설명 (0) | 2022.07.24 |
Junit5 Assertions / Assumptions 설명 (0) | 2022.07.24 |