Search
Duplicate

Mockito

목차

소개

Mock: 객체와 비슷하게 동작하지만 프로그래머가 직접 그 객체의 행동을 관리하는 객체
Mockito: Mock객체를 쉽게 만들고 관리하고검증할 수 있는 방법을 제공해주는 프레임워크
협업 혹은 외부 API를 사용해서 개발을 해야하는 경우가 많다.
그런데 협업하는 다른 개발자가 해당 API를 아직 구현하지 않았거나 외부 API가 현재 제공되지 않는다면, 그래서 내가 필요한 데이터를 받지 못한다면 어떻게 해야할까? 마감 일정은 정해져있기에 마냥 기다릴 수는 없기에 우리는 API를 사용하여 적절한 값을 반환하는 인터페이스를 만든 뒤 프록시를 이용해서 이를 해결할 수 있다.
그럼, 테스트 케이스를 작성할때는 어떻게 해야할까? 이 경우 Mock프레임워크를 이용해 요청을 Mocking해서 실제 응답결과와 비슷한 결과를 미리 개발자가 작성해서 응답하도록 할 수 있다.
그리고, 내가 테스트를 해야하는 내용이 DB의 내용을 조회하고 변경하고 삭제를 해야한다면 어떨까?
실제 DB를 건드리기는 위험하고, 개발중에는 해당 DB가 아직 구축되어있지 않을수도 있다.
이럴경우 Mockito와 같은 Mock프레임워크를 이용하면 안전하고 독립적인 테스트가 가능해진다.

참고: 단위테스트의 고찰

개발자마자 테스트의 고립성의 수준을 어느정도로 유지해야 단위테스트인가에 대해서 생각이 다르다. 어떠한 엄격한 개발자들은 모든 의존성을 끊어야 하기 때문에 모든 의존성에 대해서 mocking을 해야한다는 개발자들이 있다. 한편, 어떠한 개발자는 단위를 생각할 때 단위를 어떠한 행동의 단위로 생각할수도 있다고 한다. 행위를 단위로 보기때문에 행위에 연관된 모든 객체들은 같이 테스트가 진행되도 괜찮다고 생각한다.
여기에 정답은 없기에 개발을 시작 할 때 단위테스트에서 단위의 범위와 Mock을 어디까지 해야할 지에 대해서는 팀 별로 논의를 하는것이 좋다.

시작하기

스프링 부트 환경에서는 Mockito는 기본으로 sprring-boot-starter-test 의존성에 같이 포함되어 있다. 하지만, 만약 sprring-boot-starter-test가 없다면 mockito-junit-jupitermockito-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>
XML
복사
스프링부트가 없다면 위 의존성을 추가해주면 된다.
Mock을 처음 보는 사람입장에서는 이를 사용하는게 상당히 어려워보일 수 있지만, 실제로 사용법은 상당히 간단하다. 다음 방법만 안다면 몹시 쉽게 사용 가능하고 다음 챕터부터 학습해볼 것이다.
Mock을 만드는 방법
Mock이 어떻게 동작해야 하는지 관리하는 방법
Mock의 행동을 검증하는 방법

Mockito 레퍼런스

Mock객체 만들기

Mock객체를 만드는 방법은 메서드를 통한 방법과 애노테이션을 이용하는 방법이 있는데 하나씩 알아보자.

mock()메서드를 이용해 만드는 방법

MemberService memberService = Mockito.mock(MemberService.class); StudyRepository studyRepository = mock(StudyRepository.class);
Java
복사
⇒ Mockito 클래스는 static하게 선언해서 생략이 가능하다.

@Mock 애노테이션을 이용하는 방법

1.
JUnit 5 확장 모델로 MockitoExtension을 사용하도록 한다.
@ExtendWith(MockitoExtension.class) class StudyServiceTest { ... }
Java
복사
MockitoExtension 을 확장모델로 사용하지않으면 @Mock 애노테이션 사용이 불가능하다.
2.
필요한곳에 @Mock 애노테이션을 사용
@ExtendWith(MockitoExtension.class) class StudyServiceTest { @Mock MemberService memberService; ... @Test void createStudyService(memberService, @Mock StudyRepository studyRepository) { StudyService studyService = new StudyService(memberService, studyRepository); assertNotNull(studyService); } }
Java
복사
⇒ 원하는 곳에 @Mock 애노테이션을 사용하여 Mock객체를 만들어줄 수 있다.
⇒ 매개변수로 받는 곳에 @Mock 애노테이션을 붙히면 Mock 객체가 생성되어 주입된다.

Mock객체 Stubbing

Mock객체를 생성해준 것 까지는 좋지만, 말 그대로 빈 껍데기이기 때문에 실제로 메서드 호출을하면 제대로 동작을 하지 않는다. 모든 Mock객체의 기본적인 행동은 다음과 같다.
Null을 반환한다.
Primitive 타입은 기본 Primitive 값을 반환한다.
콜렉션은 비어있는 콜렉션을 반환한다.
void 메소드는 예외를 던지지않고 아무런 일도 발생하지 않는다.
Method Stub : 기존 코드를 흉내내어 아직 기존 코드를 임시로 대치하는 역할을 수행함으로써 아직 구현되지 않았거나 의존성을 떨어트리고 독립적인 테스트를 수행해야 하는경우 이점을 가질 수 있다.

코드

StudyServiceTest
:Mock객체를 사용해 Stubbing 해보기
@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)); } }
Java
복사
when(memberService.findById(any())).thenReturn(Optional.of(member));
memberService.findById라는 메서드를 호출할 때 안에 어떤 값을 넣어도 뒤에 체이닝된 메서드 thenResult에 인자값으로 전달한 값인 Optional.of(member) 가 반환된다.
여기서 any()ArgumentMatchers에서 제공하는 메서드로 만약 이게아니라 명시적으로 1L이나 2L같은 값을 넣으면 해당 값을 넣을때만 선언한 결과가 반환 될 것이다.
doThrow(new IllegalArgumentException()).when(memberService).validate(2L);
memberService에서 validate 메서드에 2L을 인자로 전달해 호출할 때 new IllegalArgumentException예외가 발생한다는 것으로 주로 void 와 같이 반환타입이 없는 메서드에서 특정 예외를 발생시키고자 할 때 사용한다.
만약 반환타입이 있는 메서드에서 예외를 발생시키고자 한다면 thenThrow 메서드를 사용하면 된다.
when(memberService.findById(any()).thenReturn(...).thenThrow(...).thenReturn(...);
⇒ 동일한 메서드를 반복해서 호출할 때 각각 다르게 행동하도록 조작할수도 있다. 위 코드에서는 첫 번째 호출할때는 정상적으로 조회가 되고 두 번째에는 RuntimeException 예외 발생, 그리고 세 번쨰 호출시에는 Optional.empty()가 반환되도록 했다.

참고: Argument matchers

Method Stub을 할 때 메서드의 특정 인자값에 반응하게 하는게 아니라 좀 더 유연하게 인자값을 설정하고싶을 때 Argument matchers를 사용하면 다양한 인자값에 대처할 수 있다.

Mock 객체 확인

스터디에 대한 비즈니스로직이 있는 스터디서비스(StudyService)객체가 있고, 여기서 스터디를 저장하고 특정 회원을 해당 강의의 주인(강사)로 등록하는 메서드인 createNewStudy()라는 메서드가 있다고하자.
그럼, 우리는 이 메서드의 결과는 확인할 수 있지만, 만약 이 안에서 내가 임의의 Mock객체의 임의의 메서드(ex: notify)가 수행되는것을 횟수, 시점등을 확인하고싶다면 어떻게 해야할까?
Mockito에서는 verify라는 메서드를 통해 Mock 객체의 메서드 호출을 확인할 수 있다.
verify라는 메서드를 이용해 단순히 Mock 객체의 동작 여부 뿐 아니라 순서나 시간내에 실행되었는지, 그리고 심지어 특정 메서드 실행 이후 더이상 Mock이 실행되지않았는지도 확인을 할 수 있다.

코드

StudyService.createNewStudy() 메서드부분
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; }
Java
복사
StudyServiceTest - 기본적인 Mock객체 동작 확인 테스트
package me.whiteship.inflearnthejavatest.study; import me.whiteship.inflearnthejavatest.domain.Member; import me.whiteship.inflearnthejavatest.domain.Study; import me.whiteship.inflearnthejavatest.member.MemberService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.*; @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()); } }
Java
복사
verify(memberService, times(1)).notify(study);
⇒ memberService의 notify()메서드에 study 매개변수가 전달되어 호출되는게 1번이어야 한다는 의미이다.
verify(memberService, times(1)).notify(member);
⇒ memberService의 notify()메서드에 member 매개변수가 전달되어 호출되는게 1번이어야 한다는 의미이다.
verify(memberService, never()).validate(anyLong());
⇒ memberService의 validate() 메서드가 수행되지 않는지 확인하는 메서드이다.
StudyServiceTest - Mock객체 동작동 순서까지 확인하는 테스트
@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); } }
Java
복사
InOrder inOrder = inOrder(memberService);
⇒ memberService의 동작확인을 순서에 맞춰 하기위해 inOrder메서드를 사용한다. 내가 모든 동작을 확인할 필요는 없지만, 몇몇 동작에 대해 순서를 확인할 수 있다.
inOrder.verify(memberService, times(1)).notify(study);
⇒ memberService의 notify(study) 메서드가 처음 수행되야 한다.
inOrder.verify(memberService, times(1)).notify(member);
⇒ memberService의 notify(member) 메서드가 그 다음으로
verifyNoMoreInteractions(memberService);
⇒ 위에서 확인한 동작 이후로 memberService에 어떠한 동작도 있어서는 안된다.

참고: Verifying exact number of invocations

Mock 객체의동작확인에 대해 더 자세한 내용은 공식 API 문서를 참고하면 된다.

Mockito BDD Style API

BDD(Behavior Driven Development)

애플리케이션이 어떻게 "행동" 해야 하는지에 대해서 공통된 이해를 구성하는 방법.
즉, 행위 기반 테스트라고 할 수 있다.
Mockito 프레임워크에서는 BddMockito라는 클래스를 통해 BDD스타일의 API도 제공하기 때문에 개발자가 BDD 스타일의 테스트를 작성하고 싶다면 편하게 사용할 수 있다.

행동(behavior)에 대한 스펙

Title
Narrative
As a / I want / so that
Acceptance criteria
Given / When / Then

어떻게 변하는가?

기존: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);

코드

package me.whiteship.inflearnthejavatest.study; import me.whiteship.inflearnthejavatest.domain.Member; import me.whiteship.inflearnthejavatest.domain.Study; import me.whiteship.inflearnthejavatest.member.MemberService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.times; @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(); } }
Java
복사

다음 챕터로

이전 챕터로