본문 바로가기
스프링/JUnit

Mockito란? Mockito 사용하기

by 책 읽는 개발자_테드 2021. 11. 15.
반응형

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

https://github.com/ShimSeoungChul/fastcampus-eatgo/blob/main/eatgo-restaurant-api/src/test/java/kr/co/fastcampus/eatgo/application/ReservationServiceTests.java

 

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

반응형

댓글