Mockito 소개
Mock?
mock 단어의 뜻은 모조품이다
진짜 객체와 비슷하게 동작하지만 개발자가 직접 그 객체의 행동을 관리하는 객체
Mockito: Mock 객체를 쉽게 만들고 관리하고 검증할 수 있는 방법을 제공한다
즉, Mockito는 Mock 프레임워크
자바 개발자는 Mockito 프레임워크를 가장 많이 사용한다.
Mock 프레임워크를 사용하는 이유
테스트하려는 클래스가 의존하는 객체(DB, 외부 API 등)를 실제로 호출하지 않고 격리된 상태에서 테스트할 수 있다.
ex) 데이터베이스 연결 없이 Repository / Service 계층 테스트 수행
이를 통해 테스트에서 외부 시스템 의존성이 제거된다.
외부 API나 시스템(서드파티 서비스)가 동작하지 않거나 접근 불가능한 상태에서도 Mock 객체로 대신해 테스트를 진행할 수 있다.
Mock 객체는 실제 객체와 다르게 시간이 오래 걸리는 작업(파일 IO, 네트워크 요청 등)을 하지 않으므로 테스트 속도가 빨라진다.
Mockito 시작하기
스프링 부트 2.2+ 프로젝트 생성시 spring-boot-starter-test에서 자동으로 Mockito 추가해 준다
다음 세가지만 알면 Mock을 활용한 테스트를 쉽게 작성할 수 있다
Mock 만드는 방법
Mock이 어떻게 동작해야 하는지 관리하는 방법
Mock의 행동을 검증하는 방법
Mockito 레퍼런스
https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
Mock 객체 만들기
public class StudyService {
private final MemberService memberService;
private final StudyRepository repository;
public StudyService(MemberService memberService, StudyRepository repository) {
this.memberService = memberService;
this.repository = repository;
}
public Study createNewStudy(Long memberId, Study study){
Optional<Member> member = memberService.findById(memberId);
study.setOwnerId(member.orElseThrow(() ->
new IllegalArgumentException("Member doesn't exist for id: '" + memberId + "'")).getId());
return repository.save(study);
}
}
StudyService에 대한 테스트 코드를 작성하자
현재 StudyService 클래스는 MemberService, StudyRepository에 의존한다.
StudyRepository는 JpaRepository를 확장한 Repository (인터페이스)
MemberService는 인터페이스이다.
StudyRepository 같은 경우 스프링과 같이 테스트하여 StudyRepository를 상속 받는 구현체를 빈으로 받아와 테스트할 수 있지만 스프링 없이 단위 테스트를 하고 싶다면? mocking이 필요하다.
Mockito.mock 메소드
class StudyServiceTest {
@Test
void createStudyService(){
MemberService memberService = mock(MemberService.class);
StudyRepository studyRepository = mock(StudyRepository.class);
StudyService studyService = new StudyService(memberService, studyRepository);
assertNotNull(studyService);
}
}
Mockito.mock 메소드를 사용해서 MeberService, StudyRepository mock 객체를 만들었다.
import static org.mockito.Mockito.*;
@Mock 애노테이션으로 만드는 방법
JUnit 5 extension으로 MockExtension을 사용해야 한다
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Mock
MemberService memberService;
@Mock
StudyRepository studyRepository;
@Test
void createStudyService(){
StudyService studyService = new StudyService(memberService, studyRepository);
System.out.println(studyService);
assertNotNull(studyService);
}
}
현재는 mock 객체를 필드로 주입
테스트 메소드에 파라미터로도 사용이 가능하다.
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Test
void createStudyService(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
StudyService studyService = new StudyService(memberService, studyRepository);
System.out.println(studyService);
assertNotNull(studyService);
}
}
마찬가지로 JUnit 5 extension으로 MockExtension을 사용해야 한다
Mock 객체 Stubbing
Mock 객체 stubbing이란 mock 객체의 특정 메소드 호출에 대해 예상되는 동작(반환 값, 예외 던지기)등을 미리 정의해 두는 것을 의미한다.
이를 통해 테스트 코드에서 원하는 시나리오를 시뮬레이션할 수 있다.
mock 객체의 stubbing 되지 않은 기본 행동
Object 타입은 Null 반환 (Optional 타입은 Optional.empty 리턴)
Primitive 타입은 기본 Primitive 값
컬렉션은 비어있는 컬렉션
void 메소드는 예외를 던지지 않고 아무런 일도 발생하지 않는다.
MemberService interface
public interface MemberService {
Optional<Member> findById(Long memberId);
void validate(Long memberId);
}
Test
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Test
void createStudyService(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
StudyService studyService = new StudyService(memberService, studyRepository);
Optional<Member> findMember = memberService.findById(1L);
assertEquals(Optional.empty(), findMember);
memberService.validate(2L);
System.out.println(studyService);
assertNotNull(studyService);
}
}
Optional 반환 타입은 Optional.empty() 타입을 반환, void는 아무런 동작을 하지 않는 것을 확인할 수 있었다.
stubbing
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Test
void createNewStudy(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
StudyService studyService = new StudyService(memberService, studyRepository);
Member member = new Member();
member.setId(1L);
member.setEmail("baejeu@naver.com");
// stubbing
when(memberService.findById(1L)).thenReturn(Optional.of(member));
Study study = new Study(10, "java");
Optional<Member> findById = memberService.findById(1L);
assertEquals("baejeu@naver.com", findById.get().getEmail());
studyService.createNewStudy(1L, study);
}
}
Mockito.when() 메소드를 사용해서 meberService.findById(1L)를 호출했을 때
미리 정의한 Member가 반환되도록 stubbing 하였다.
만약 createNewStudy 메소드에 정의하지 않은 memberId(2L)를 넘기면 어떤 결과가 나올까?
public Study createNewStudy(Long memberId, Study study){
Optional<Member> member = memberService.findById(memberId);
study.setOwnerId(member.orElseThrow(() ->
new IllegalArgumentException("Member doesn't exist for id: '" + memberId + "'")).getId());
return repository.save(study);
}
member.orElseThrow()에서 정의해둔 IllegalArgumentException이 발생한다.
테스트 성공을 위해서
assertThrows 사용
assertThrows(IllegalArgumentException.class, () -> studyService.createNewStudy(2L, study));
ArgumentMatchers
Mockito에서 Mock 객체의 메소드 호출 시 전달되는 인자를 유연하게 제어할 수 있도록 지원하는 클래스이다.
특정 값뿐만 아니라 임의의 값, 특정 조건에 맞는값에 대해서도 동작을 정의할 수 있다.
any()
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Test
void createNewStudy(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
StudyService studyService = new StudyService(memberService, studyRepository);
Member member = new Member();
member.setId(1L);
member.setEmail("baejeu@naver.com");
// stubbing
when(memberService.findById(any())).thenReturn(Optional.of(member));
assertAll(
() -> assertEquals(member, memberService.findById(1L).get()),
() -> assertEquals(member, memberService.findById(2L).get()),
() -> assertEquals(member, memberService.findById(null).get())
);
}
}
findById에 어떤 값이든 상관 없이 미리 만들어 놓은 member를 반환한다.
anyLong(), anyFloat(), anyInt() 기본 Matcher
anyList(), anyMap(), anyCollection() 컬렉션 Matcher
argThat() 조건 Matcher 등등 다양한 Matcher를 지원한다.
예외 만들기, Stubbing 순서
Subbing void methods with exceptions
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Test
void createNewStudy(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
StudyService studyService = new StudyService(memberService, studyRepository);
Member member = new Member();
member.setId(1L);
member.setEmail("baejeu@naver.com");
when(memberService.findById(any())).thenReturn(Optional.of(member));
when(memberService.findById(1L)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, () -> memberService.findById(1L));
doThrow(new IllegalArgumentException()).when(memberService).validate(2L);
assertThrows(IllegalArgumentException.class, () -> memberService.validate(2L));
memberService.validate(3L);
}
}
Mockito Stubbing 순서에 따라 동작이 달라진다.
ArgumentMatcher any()를 통해서 thenReturn(member)를 stubbing 했지만
다시 thenThrow를 통해서 memberId가 1L인 findById 동작을 RuntimeException을 던지게 했기 때문에
memberService.findById(1L) 했을 때 RuntimeException이 발생한다.
즉, 우선순위가 존재하는 게 아닌 정의된 순서가 중요하다
호출 순서에 따라 다른 Stubbing
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Test
void createNewStudy(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
StudyService studyService = new StudyService(memberService, studyRepository);
Member member = new Member();
member.setId(1L);
member.setEmail("baejeu@naver.com");
when(memberService.findById(anyLong()))
.thenReturn(Optional.of(member))
.thenThrow(RuntimeException.class)
.thenReturn(Optional.empty());
assertEquals(member , memberService.findById(1L).get());
assertThrows(RuntimeException.class, () -> memberService.findById(2L));
assertEquals(Optional.empty(), memberService.findById(3L));
assertEquals(Optional.empty(), memberService.findById(4L));
}
}
첫 번째 호출: Optional.of(member)
두 번째 호출: throw RuntimeException
세 번째 호출: Optional.empty()
네 번째 호출: 체이닝된 thenReturn, thenThrow가 더 이상 적용되지 않으므로 Mockito 기본 동작인 Optional.empty()가 반환된다.
즉, 체이닝으로 정의된 횟수만큼 반환하고 그 이후로는 Mockito 기본 동작을 한다.
Mock 객체 확인
MemberService
public interface MemberService {
Optional<Member> findById(Long memberId);
void validate(Long memberId);
void notify(Study newStudy);
void notify(Member member);
}
StudyService
public class StudyService {
private final MemberService memberService;
private final StudyRepository repository;
public StudyService(MemberService memberService, StudyRepository repository) {
this.memberService = memberService;
this.repository = repository;
}
public Study createNewStudy(Long memberId, Study study){
Optional<Member> member = memberService.findById(memberId);
study.setOwnerId(member.orElseThrow(() ->
new IllegalArgumentException("Member doesn't exist for id: '" + memberId + "'")).getId());
Study newStudy = repository.save(study);
memberService.notify(newStudy);
memberService.notify(member.get());
return newStudy;
}
}
memberService에서 member를 찾고 study에 owner를 set 한다.
set된 study를 StudyRepository에 저장하고 memberService에 notify()를 호출한다.
이번 테스트에서는 notify가 정상적으로 호출되었는지 확인(verify)를 하겠다.
verify
@Test
void notifyTest(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
Member member = new Member();
member.setId(1L);
member.setEmail("baejeu@naver.com");
when(memberService.findById(1L)).thenReturn(Optional.of(member));
Study study = new Study(100, "자바 스터디");
when(studyRepository.save(study)).thenReturn(study);
StudyService studyService = new StudyService(memberService, studyRepository);
studyService.createNewStudy(1L, study);
// notify 호출 검증
verify(memberService, times(1)).notify(study);
// validate 호출 검증
verify(memberService, never()).validate(any());
}
verify(Mock, VerificationMode).메소드(ArgumentsMatcher)
notify(Study) 호출을 verify를 통해서 검증하였다.
validate 메소드는 호출하지 않는다. 그래서 never()를 사용
StudyService의 createNewStudy 메소드 로직 흐름은
memberService.notify(Study)가 먼저 호출되고 그다음에 memberService.notify(Member)가 호출된다.
이번에는 순서에 맞게 호출되었는지 검증해 보자
InOrder 순서 verify
@Test
void notifyTest(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
Member member = new Member();
member.setId(1L);
member.setEmail("baejeu@naver.com");
when(memberService.findById(1L)).thenReturn(Optional.of(member));
Study study = new Study(100, "자바 스터디");
when(studyRepository.save(study)).thenReturn(study);
StudyService studyService = new StudyService(memberService, studyRepository);
studyService.createNewStudy(1L, study);
// notify 호출 검증
verify(memberService, times(1)).notify(study);
// validate 호출 검증
verify(memberService, never()).validate(any());
// 순서 검증
InOrder inOrder = inOrder(memberService);
inOrder.verify(memberService).notify(study);
inOrder.verify(memberService).notify(member);
}
순서 검증은 InOrder를 통해서 한다.
만약 inOrder.verify(memberService) 순서가 바뀐다면 테스트는 실패한다.
verifyNoMoreInteractions
@Test
void notifyTest(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
Member member = new Member();
member.setId(1L);
member.setEmail("baejeu@naver.com");
when(memberService.findById(1L)).thenReturn(Optional.of(member));
Study study = new Study(100, "자바 스터디");
when(studyRepository.save(study)).thenReturn(study);
StudyService studyService = new StudyService(memberService, studyRepository);
studyService.createNewStudy(1L, study);
// notify 호출 검증
verify(memberService, times(1)).notify(study);
verifyNoMoreInteractions(memberService);
}
verifyNoMoreInteractions()는 mock 객체에 대한 action이 없어야 함을 의미한다.
하지만 현재 createNewStudy 로직은 notify(study)이후 notify(member)를 호출한다.
그래서 테스트가 실패한다.
더 많은 내용
특정 메소드가 특정 매개변수로 몇번 호출 되었는지, 최소 한번은 호출 됐는지, 전혀 호출되지 않았는지
Verifying exact number of invocations
어떤 순서대로 호출했는지
특정 시간 이내에 호출됐는지
특정 시점 이후에 아무 일도 벌어지지 않았는지
BDD 스타일 Mockito API
BDD 애플리케이션이 어떻게 “행동”해야 하는지에 대한 공통된 이해를 구성하는 방법으로, TDD에서 창안했다.
Mockito는 BddMockito라는 클래스를 통해 BDD 스타일의 API를 제공한다.
@Test
void bddTest(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
Member member = new Member();
member.setId(1L);
member.setEmail("baejeu@naver.com");
when(memberService.findById(1L)).thenReturn(Optional.of(member));
Study study = new Study(100, "자바 스터디");
when(studyRepository.save(study)).thenReturn(study);
StudyService studyService = new StudyService(memberService, studyRepository);
studyService.createNewStudy(1L, study);
// notify 호출 검증
verify(memberService, times(1)).notify(study);
verifyNoMoreInteractions(memberService);
}
이전에 verify를 연습했던 테스트 코드이다.
이를 Given / When / Then BDD 스타일의 코드로 바꿔보자
import static org.mockito.BDDMockito.*;
@Test
void bddTest(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
// given
Member member = new Member();
member.setId(1L);
member.setEmail("baejeu@naver.com");
given(memberService.findById(1L)).willReturn(Optional.of(member));
Study study = new Study(100, "자바 스터디");
given(studyRepository.save(study)).willReturn(study);
StudyService studyService = new StudyService(memberService, studyRepository);
// when
studyService.createNewStudy(1L, study);
// then
then(memberService).should(times(1)).notify(study);
then(memberService).should(never()).validate(any());
then(memberService).shouldHaveNoMoreInteractions();
}
Mockito: when → BddMockito: given
Mockito: verify → BddMockito: then
참고 자료
[인프런] 백기선님 더 자바, 애플리케이션을 테스트하는 다양한 방법
'Spring' 카테고리의 다른 글
[JUnit 5] 정리 (2) | 2024.11.25 |
---|