Mock : 객체와 비슷하게 동작하지만 프로그래머가 직접 그 객체의 행동을 관리하는 객체Mockito : Mock 객체를 쉽게 만들고 검증할 수 있는 방법을 제공해 주는 프레임워크협업 혹은 외부 API 사용 시, 해당 도메인 API가 아직 구현되지 않았거나 외부 API 도입이 늦춰지는 경우가 있다. 이럴 때 적절한 값을 반환하는 인터페이스를 만든 뒤 프록시를 이용해 해결할 수 있다.
그럼, 테스트 케이스를 작성할 때는 어떻게 해야 할까 ❓
이 경우 Mock 프레임워크를 이용해 요청을 Mocking해 실제 응답 결과와 비슷한 결과를 작성해서 응답하도록 할 수 있다.
또한 개발 중 DB가 아직 구축되어 있지 않을 시, Mockito와 같은 Mock 프레임워크를 이용하면 안전하고 독립적인 테스트가 가능해진다.
개발자마다 테스트 고립성 수준에 대해 생각이 다르다.
모든 의존성을 끊어야 하기 때문에 모든 의존성에 대해서 mocking을 해야 한다는 의견도 있고,
단위를 생각할 때 단위를 어떠한 행동의 단위로 생각하는 경우도 있다.
행위를 단위로 보기 때문에 행위에 연관된 객체들은 같이 테스트가 진행돼도 괜찮다고 생각한다.
정답은 없기에 개발을 시작할 때 단위 테스트에서 단위의 범위와 Mock을 어디까지 해야 할지에 대해 논의하고 진행하자.
스프링 부트 환경에서는 Mockito는 기본으로 spring-boot-starter-test 의존성에 같이 포함되어 있다. 만약 spring-boot-starter-test가 없다면 mockito-junit-jupiter와 mockito-core 라이브러리를 추가해 주면 된다.
<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 객체를 만드는 방법은 메서드를 통한 방법과 어노테이션을 이용하는 방법이 있다.
MemberService memberService = Mockito.mock(MemberService.class);
StudyRepository studyRepository = mock(StudyRepository.class);
Mockito 클래스는 static 하게 선언해서 생략 가능JUnit 5 확장 모델로 MockitoExtension을 사용하도록 한다.
MockitoExtension을 확장 모델로 사용하지 않으면 @Mock 어노테이션 사용이 불가능하다.
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
...
}
@Mock 어노테이션 사용@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Mock MemberService memberService;
...
@Test
void createStudyService(memberService, @Mock StudyRepository studyRepository) {
StudyService studyService = new StudyService(memberService, studyRepository);
assertNotNull(studyService);
}
}
@Mock 어노테이션을 사용하여 Mock 객체를 만들어줄 수 있다.@Mock 어노테이션을 붙이면 Mock 객체가 생성되어 주입된다.Mock 객체를 생성만 한다면 빈 껍데기이기 때문에 제대로 동작을 하지 않는다.
모든 Mock 객체의 기본적인 행동은 다음과 같다.
Primitive 타입은 기본 Primitive 값을 반환한다.Method Stub기존 코드를 흉내 내어 임시로 대치하는 역할을 수행함으로써 아직 구현되지 않았거나,
독립적인 테스트를 수행해야 하는 경우 이점을 가질 수 있다.
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Test
void createStudyService(@Mock MemberService memberService,
@Mock StudyRepository studyRepository) {
Member member = new Member();
member.setId(1L);
member.setEmail("catsbi@email.com");
when(memberService.findById(any())).thenReturn(Optional.of(member));
doThrow(new IllegalArgumentException()).when(memberService).validate(2L);
Optional<Member> optional = memberService.findById(1L);
assertNotNull(optional.get());
assertEquals(optional.get(), member);
assertThrows(IllegalArgumentException.class, () -> memberService.validate(2L));
when(memberService.findById(any()))
.thenReturn(Optional.of(member))
.thenThrow(new RuntimeException())
.thenReturn(Optional.empty());
Optional<Member> findById = memberService.findById(1L);
assertEquals(findById.get(), member);
assertThrows(RuntimeException.class, ()-> memberService.findById(1L));
assertEquals(Optional.empty(), memberService.findById(1L));
}
}
memberService.findById 메서드를 호출할 때 안에 어떤 값을 넣어도 뒤에 체이닝된 thenResult 메서드에 인자 값으로 전달한 Optional.of(member)가 반환된다. 여기서 any()는 ArgumentMatchers에서 제공하는 메서드, 만약 명시적으로 1L이나 2L같은 값을 넣으면 해당 값을 넣을 때만 선언한 결과 반환
memberService에서 validate 메서드에 2L을 인자로 전달해 호출할 때 new IllegalArgumentException 예외가 발생한다는 것으로 주로 void와 같이 반환 타입이 없는 메서드에서 특정 예외를 발생시키고자 할 때 사용한다.만약 반환 타입이 있는 메서드에서 예외를 발생시키고자 한다면 thenThrow 메서드를 사용하면 된다.
RuntimeException 예외 발생, 그리고 세 번째 호출 시에는 Optional.empty()가 반환되도록 했다. Argument matchers를 사용하면 다양한 인자 값에 대처할 수 있다. 스터디 비즈니스 로직이 있는 StudyService 객체가 있고, 스터디를 저장하고 특정 회원을 해당 강의 강사로 등록하는 createNewStudy 메서드가 있다고 하자.
Mock 객체의 임의의 메서드(ex: notify)가 수행되는 것을 횟수, 시점 등을 확인하고 싶다면 어떻게 해야 할까 ❓
Mockito에서는 verify라는 메서드를 통해 Mock 객체의 메서드 호출을 확인할 수 있다.
verify라는 메서드를 이용해 단순히 Mock 객체의 동작 여부뿐 아니라 순서나 시간 내에 실행되었는지, 그리고 심지어 특정 메서드 실행 이후 Mock이 실행되지 않았는지도 확인을 할 수 있다.
public Study createNewStudy(Long memberId, Study study) {
Optional<Member> member = memberService.findById(memberId);
if (member.isPresent()) {
study.setOwnerId(memberId);
} else {
throw new IllegalArgumentException("Member doesn't exist for id: '" + memberId + "'");
}
Study newstudy = repository.save(study);
memberService.notify(newstudy);
memberService.notify(member.get());
return newstudy;
}
@ExtendWith(MockitoExtension.class)
public class StudyServiceTest {
@DisplayName("스터디 만들기")
@Test
void createNewStudy_test(@Mock StudyRepository studyRepository,
@Mock MemberService memberService) {
StudyService studyService = new StudyService(memberService, studyRepository);
Member member = new Member();
member.setEmail("catsbi@email.com");
member.setId(1L);
Study study = new Study(10, "수학");
study.setId(2L);
when(memberService.findById(anyLong())).thenReturn(Optional.of(member));
when(studyRepository.save(study)).thenReturn(study);
studyService.createNewStudy(1L, study);
assertEquals(member.getId(), study.getOwnerId());
verify(memberService, times(1)).notify(study);
verify(memberService, times(1)).notify(member);
verify(memberService, never()).validate(anyLong());
}
}
@ExtendWith(MockitoExtension.class)
public class StudyServiceTestForPosting {
@DisplayName("스터디 만들기")
@Test
void createNewStudy_test(@Mock StudyRepository studyRepository,
@Mock MemberService memberService) {
...
InOrder inOrder = inOrder(memberService);
inOrder.verify(memberService, times(1)).notify(study);
inOrder.verify(memberService, times(1)).notify(member);
verifyNoMoreInteractions(memberService);
}
}
애플리케이션이 어떻게 "행동" 해야 하는지에 대해서 공통된 이해를 구성하는 방법.
즉, 행위 기반 테스트라고 할 수 있다.
Mockito 프레임워크에서는 BddMockito라는 클래스를 통해 BDD 스타일의 API도 제공하기 때문에 개발자가 BDD 스타일의 테스트를 작성하고 싶다면 편하게 사용할 수 있다.
💡
기존 : when(memberService.findById(1L)).thenReturn(Optional.of(member));
변경 : given(memberService.findById(1L)).willReturn(Optional.of(member));
💡
기존 : verify(memberService, times(1)).notify(study);
변경 : then(memberService).should(times(1)).notify(study);
@ExtendWith(MockitoExtension.class)
public class StudyServiceTestForPosting {
@Mock StudyRepository studyRepository;
@Mock MemberService memberService;
@DisplayName("스터디 만들기")
@Test
void createNewStudy_test() {
//given
StudyService studyService = new StudyService(memberService, studyRepository);
Member member = new Member();
member.setEmail("catsbi@email.com");
member.setId(1L);
Study study = new Study(10, "수학");
study.setId(2L);
given(memberService.findById(anyLong())).willReturn(Optional.of(member));
given(studyRepository.save(study)).willReturn(study);
//when
studyService.createNewStudy(1L, study);
//then
assertEquals(member.getId(), study.getOwnerId());
then(memberService).should(times(1)).notify(study);
then(memberService).should(times(1)).notify(member);
then(memberService).shouldHaveNoMoreInteractions();
}
}