Mockito란?
· Mock 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 프레임워크
- Mock: 진짜 객체와 비슷하게 동작하지만, 프로그래머가 직접 행동을 관리하는 객체
- 공홈 https://site.mockito.org/
· 테스트를 작성하는 자바 개발자의 45%가 사용하는 Mock 프레임워크
- 2021년 젯브레인 설문조사 기준 https://www.jetbrains.com/lp/devecosystem-2021/java/
· 대체제: EasyMock, JMock
· 애플리케이션에서 데이터베이스, 외부 API 등을 테스트할 때, 해당 제품들이 어떻게 작동하는지 항상 사용하면서 테스트를 작성한다면 매우 불편할 것이다. 이럴 때 어떻게 작동하는지 예측을 하여 Mock 객체를 만들어서 사용하면, 편리한 테스트가 가능하다.
- ex) dao 또는 repository가 어떻게 작동하는지 Mockito를 이용해 만들어 놓으면,
dao, repository 객체를 구현하기 전에도 테스트를 작성할 수 있다.
· 단위 테스트에 대한 고찰 TODO: https://martinfowler.com/bliki/UnitTest.html
- 백기선님 의견: 이미 구현되어있는 클래스를 모킹할 필요는 없다. 개발자가 컨트롤하기 힘든 외부서비스는 모킹이 필요하다.
Mockito 사용하기
· 스프링부트 2.2+ 프로젝트 생성시 spring-boot-start-test에서 자동으로 Mokito를 추가해준다.
· 스프링 부트를 쓰지 않는다면, 의존성을 직접 추가한다.
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency
* mockito-core: mockito가 제공해주는 기본적인 라이브러리
* mockito-junit-jupiter: junit 테스트에서 mockito를 연동해서 사용할 수 있는 junit 확장 구현체
· Mock을 활용한 테스트를 작성할 때 알아야할 내용
1. Mock을 만드는 방법
2. Mock이 어떻게 동작해야 하는지 관리하는 방법
3. Mock의 행동을 검증하는 방법
Mock 객체 만들기
· 테스트를 작성하기 위한 스프링 부트 메이븐 프로젝트를 생성한다.
Spring initializer에서 Spring Web, Spring Data JPA 의존성을 추가한 후 GENERATE 버튼을 클릭한다.
· 프로젝트를 다음과 같은 구조로 만든다.
코드 내용: https://github.com/keesun/inflearn-the-java-test
import java.util.Optional;
import com.thejavatest.domain.Member;
public interface MemberService {
Optional<Member> findById(Long memberId);
void validate(Long memberId);
void notify(Study newstudy);
void notify(Member member);
}
import org.springframework.data.jpa.repository.JpaRepository;
import com.thejavatest.domain.Study;
public interface StudyRepository extends JpaRepository<Study, Long> {
}
· 코드를 모두 작성했다면, 아래 StudyService 코드에 대한 테스트를 만들어보자.
import java.util.Optional;
import com.thejavatest.domain.Member;
import com.thejavatest.domain.Study;
import com.thejavatest.member.MemberService;
public class StudyService {
private final MemberService memberService;
private final StudyRepository repository;
public StudyService(MemberService memberService, StudyRepository repository){
assert memberService != null;
assert repository != null;
this.memberService = memberService;
this.repository = repository;
}
public Study createNewStudy(Long memberId, Study study){
Optional<Member> member = memberService.findById(memberId);
study.setOwner(member.orElseThrow(() -> new IllegalArgumentException("Member doesn't exist for id: '" + memberId + "'")));
return repository.save(study);
}
}
import org.junit.jupiter.api.Test;
class StudyServiceTest {
@Test
void createStudyService(){
// MemberService, StudyRepository의 인터페이스만 있고 구현체가 없기 때문에
// StudyService의 인스턴스를 만들 수 없다.
StudyService studyService = new StudyService();
}
}
· 위의 StudyService 코드처럼 의존하고 있는 타입의 인터페이스만 존재하고, 구현체가 없지만
작성하는 코드가 잘 작동하는 테스트를 하고 싶을때가 Mock 객체를 사용하기 적절한 케이스다.
· 위 문제를 해결하기 위해 Mockito.mock() 메서드에 만들고 싶은 Type을 매개변수로 넣어주면,
원하는 객체를 만들 수 있다.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.thejavatest.member.MemberService;
class StudyServiceTest {
@Test
void createStudyService(){
MemberService memberService = mock(MemberService.class);
StudyRepository studyRepository = mock(StudyRepository.class);
StudyService studyService = new StudyService(memberService, studyRepository);
assertNotNull(studyService);
}
}
· 위 코드를 MockitoExtention을 사용하여 간소화 할 수 있다.
- 클래스 위에 @ExtendWith(MockitoExtension.class)를 작성하고,
memberService, studyReposiory 변수를 클래스 변수로 만들고 @Mock 애노테이션을 붙인다.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import com.thejavatest.member.MemberService;
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Mock
MemberService memberService;
@Mock
StudyRepository studyRepository;
@Test
void createStudyService(){
StudyService studyService = new StudyService(memberService, studyRepository);
assertNotNull(studyService);
}
}
· Mock 객체를 특정 메서드 내에서만 사용하고 싶다면, 다음과 같이 파라미터로 설정할 수 있다.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import com.thejavatest.member.MemberService;
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Test
void createStudyService(@Mock MemberService memberService, @Mock StudyRepository studyRepository){
StudyService studyService = new StudyService(memberService, studyRepository);
assertNotNull(studyService);
}
}
Mock 객체 Stubbing (행동 정의하기)
· 모든 Mock 객체의 행동
- Optional 타입은 Optional.empty를 나머지 타입은 Null을 리턴한다. (즉, 비어있다 )
- Primitive 타입은 기본 Primitive 값
- 콜렉션은 비어있는 콜렉션
- Void 메서드는 예외를 던지지 않고 아무런 일도 발생 x
· Mock 객체를 조작하면,
- 특정한 매개변수를 받은 경우 특정한 값을 리턴하거나 예외를 던지도록 만들 수 있다.
- Void 메서드가 특정 매개변수를 받거나 호출된 경우 예외를 발생 시킬 수 있다.
- 메서드가 동일한 매개변수로 여러번 호출될 때 각기 다르게 행동하도록 조작할 수 있다.
· Mockito의 when() 메서드를 이용하여 Mock 객체의 행동을 정의할 수 있다. (Stubbing 할 수 있다)
- 다음 코드는 MemberService의 findById 메서드에 매개변수 1L을 입력하여 호출하면, 특정한 member 객체가 호출된다.
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
static MemberService memberService;
static StudyRepository studyRepository;
static Member member;
static StudyService studyService;
static Study study;
@BeforeAll
static void beforeAll(){
member = new Member();
member.setId(1L);
member.setEmail("ted@email.com");
study = new Study(10, "테스트");
memberService = Mockito.mock(MemberService.class);
studyRepository = Mockito.mock(StudyRepository.class);
studyService = new StudyService(memberService, studyRepository);
}
@Test
void when_thenReturn사용(){
when(memberService.findById(1L)).thenReturn(Optional.of(member));
assertEquals("ted@email.com",memberService.findById(1L).get().getEmail());
}
}
- 다음 코드는 MemberService의 findById 메서드에 매개변수 1L을 입력하여 호출하면, 예외가 발생한다.
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
static MemberService memberService;
static StudyRepository studyRepository;
static Member member;
static StudyService studyService;
static Study study;
@BeforeAll
static void beforeAll(){
member = new Member();
member.setId(1L);
member.setEmail("ted@email.com");
study = new Study(10, "테스트");
memberService = Mockito.mock(MemberService.class);
studyRepository = Mockito.mock(StudyRepository.class);
studyService = new StudyService(memberService, studyRepository);
}
@Test
void when_thenThrow사용(){
when(memberService.findById(1L)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, ()->{
memberService.findById(1L);
});
}
}
- 다음 코드는 MemberService의 validate 메서드에 매개변수 1L을 입력하여 호출하면, 예외가 발생한다.
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
static MemberService memberService;
static StudyRepository studyRepository;
static Member member;
static StudyService studyService;
static Study study;
@BeforeAll
static void beforeAll(){
member = new Member();
member.setId(1L);
member.setEmail("ted@email.com");
study = new Study(10, "테스트");
memberService = Mockito.mock(MemberService.class);
studyRepository = Mockito.mock(StudyRepository.class);
studyService = new StudyService(memberService, studyRepository);
}
@Test
void doThrow사용(){
doThrow(new IllegalArgumentException()).when(memberService).validate(1L);
// validate() 메서드에 1L을 입력하면 예외 처리
assertThrows(IllegalArgumentException.class, ()->{
memberService.validate(1L);
});
// validate() 메서드에 1L 이외의 값을 입력하면 예외 처리 x
memberService.validate(2L);
}
}
· Mockito의 any() 메서드(Argument Matcher)를 이용하면, 모든 매개 변수에 대하여 같은 행동을 하는 Mock 객체를 만들 수 있다.
- 다음 코드는 MemberService의 findById() 메서드에 어떠한 변수가 들어와도 같은 객체를 반환한다.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Optional;
import com.thejavatest.domain.Member;
import com.thejavatest.member.MemberService;
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
static MemberService memberService;
static StudyRepository studyRepository;
static Member member;
static StudyService studyService;
static Study study;
@BeforeAll
static void beforeAll(){
member = new Member();
member.setId(1L);
member.setEmail("ted@email.com");
study = new Study(10, "테스트");
memberService = Mockito.mock(MemberService.class);
studyRepository = Mockito.mock(StudyRepository.class);
studyService = new StudyService(memberService, studyRepository);
}
@Test
void any사용(){
when(memberService.findById(any())).thenReturn(Optional.of(member));
assertEquals("ted@email.com",memberService.findById(1L).get().getEmail());
assertEquals("ted@email.com",memberService.findById(999L).get().getEmail());
}
}
· when() 메서드에서 메서드 체이닝을 통해 메서드가 동일한 매개변수로 여러번 호출될 때 각기 다르게 행동하도록 조작할 수 있다.
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
static MemberService memberService;
static StudyRepository studyRepository;
static Member member;
static StudyService studyService;
static Study study;
@BeforeAll
static void beforeAll(){
member = new Member();
member.setId(1L);
member.setEmail("ted@email.com");
study = new Study(10, "테스트");
memberService = Mockito.mock(MemberService.class);
studyRepository = Mockito.mock(StudyRepository.class);
studyService = new StudyService(memberService, studyRepository);
}
@Test
void 메서드체이닝으로_여러번_호출되는_메서드의_행동_조작(){
when(memberService.findById(any()))
.thenReturn(Optional.of(member))
.thenThrow(new RuntimeException())
.thenReturn(Optional.empty());
Optional<Member> byId = memberService.findById(1L);
assertEquals("ted@email.com",byId.get().getEmail());
assertThrows(RuntimeException.class, ()->{
memberService.findById(1L);
});
assertEquals(Optional.empty(), memberService.findById(3L));
}
}
Mockito 객체 확인
· Mock 객체가 어떻게 사용 됐는지 확인할 수 있다.
- verify() 메서드를 사용할 수 있다.
· 특정 메서드가 특정 매개변수로 몇 번 호출 되었는지 확인
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
@Mock MemberService memberService;
@Mock StudyRepository studyRepository;
static Member member;
@BeforeAll
static void beforeAll(){
member = new Member();
member.setId(1L);
member.setEmail("ted@email.com");
}
@Test
void Study객체를_생성하면_notify_1번_호출(){
StudyService studyService = new StudyService(memberService, studyRepository);
Study study = new Study(10, "테스트");
when(memberService.findById(1L)).thenReturn(Optional.of(member));
when(studyRepository.save(study)).thenReturn(study);
studyService.createNewStudy(1L, study);
// memberService에서 notify(study) 메서드가 한 번 호출 되었는지 확인
verify(memberService, times(1)).notify(study);
}
}
· 특정 메서드가 호출되지 않았는지 확인
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
static MemberService memberService;
static StudyRepository studyRepository;
static Member member;
static StudyService studyService;
static Study study;
@BeforeAll
static void beforeAll(){
member = new Member();
member.setId(1L);
member.setEmail("ted@email.com");
study = new Study(10, "테스트");
memberService = Mockito.mock(MemberService.class);
studyRepository = Mockito.mock(StudyRepository.class);
studyService = new StudyService(memberService, studyRepository);
}
@Test
void Study객체를_생성할때_validate는_호출_안됨(){
when(memberService.findById(1L)).thenReturn(Optional.of(member));
when(studyRepository.save(study)).thenReturn(study);
studyService.createNewStudy(1L, study);
// 특정 메서드가 호출되지 않았는지 확인
verify(memberService, never()).validate(any());
}
}
· 특정 메서드가 어떤 순서로 호출 되었는지 확인
@ExtendWith(MockitoExtension.class)
class StudyServiceTest {
static MemberService memberService;
static StudyRepository studyRepository;
static Member member;
static StudyService studyService;
static Study study;
@BeforeAll
static void beforeAll(){
member = new Member();
member.setId(1L);
member.setEmail("ted@email.com");
study = new Study(10, "테스트");
memberService = Mockito.mock(MemberService.class);
studyRepository = Mockito.mock(StudyRepository.class);
studyService = new StudyService(memberService, studyRepository);
}
@Test
void Study객체를_생성할때_notify_study_이후에_notify_member가_호출(){
when(memberService.findById(1L)).thenReturn(Optional.of(member));
when(studyRepository.save(study)).thenReturn(study);
studyService.createNewStudy(1L, study);
// 특정 메서드가 어떤 순서로 호출 되었는지 확인
InOrder inOrder = inOrder(memberService);
inOrder.verify(memberService).notify(study);
inOrder.verify(memberService).notify(member);
}
}
· verify 이후 다른 인터랙션이 없었다는 것을 확인
- verifyNoInteractions(mock)
· 특정 메서드가 시간 이내에 호출 되었는지 확인
- verify(mock, timeout(100),times(1)).someMethod();
@InjectMocks
해당 애노테이션이 달리면 @Spy, @Mock 어노테이션이 붙은 객체들을 주입시켜준다.
https://lemontia.tistory.com/951
Mockito BDD 스타일 API
· 행동 주도 개발(Behavior Driven Development)의 약어로,
애플리케이션이 어떻게 행동해야 하는지에 대한 공통된 이해를 구성하는 방법
· TDD에서 창안했다.
· 행동에 대한 스펙 - BDD에서는 사용자 스토리를 적는 데 어휘의 통일을 매우 중요하게 생각한다.
- Title (이야기의 제목)
- Narrative (이야기)
▶ As a (X[역할]는)
▶ I want (Y[기능]가 필요하다)
▶ so that (Z[이익]를 얻기 위해서)
- Acccceptnace criteria (인수 기준, 인수 테스트를 진행할 때 그 기준이 되는 항목)
▶ Given (주어진 상황에서)
▶ When (사건이 발생했을 때)
▶ Then (~가 수행된다)
· Mockito는 BddMockito 클래스를 통해 BDD 스타일의 API를 제공한다.
- Mockito 클래스의 when() 메서드를 BddMockito 클래스의 given() 메서드로 대체한다.
// Mockito 클래스
when(memberService.findById(1L)).thenReturn(Optional.of(member));
when(studyRepository.save(study)).thenReturn(study);
// BDDMockito 클래스
given(memberService.findById(1L)).willReturn(Optional.of(member));
given(studyRepository.save(study)).willReturn(study);
- Mockito 클래스의 verify() 메서드를 BddMockito 클래스의 then() 메서드로 대체한다.
// Mockito 클래스
verify(memberService, times(1)).notify(study);
verifyNoInteractions(memberService);
// BDDMockito 클래스
then(memberService).should(times(1)).notify(study);
then(memberService).shouldHaveNoMoreInteractions();
참고
https://zdnet.co.kr/view/?no=00000039170216
'스프링 > JUnit' 카테고리의 다른 글
JUnit 테스트에서 ApplicationRunner 실행하지 않도록 설정하기 (0) | 2022.01.09 |
---|---|
JUnit의 @BeforeAll(@Before)대신 생성자를 사용해도 괜찮지 않을까? (0) | 2021.12.23 |
[Junit] AssertJ란? (0) | 2021.10.30 |
[JUnit] 오류 해결: java.lang.IllegalStateException: Failed to load ApplicationContext (0) | 2021.08.10 |
[Junit] MockMvc 객체란? (0) | 2021.04.29 |
댓글