본문 바로가기

SearchDeveloper/애플리케이션테스트

Mockito 설명

Mockito + Junit5

이 글은 온라인 강의를 듣고 해당 내용을 직접 실습해보며 정리한 글입니다. 더 자세한 내용은 아래 강의에서 확인할 수 있습니다.

더 자바, 애플리케이션을 테스트하는 다양한 방법 - 인프런 | 강의

Mockito: Mock 을 지원하는 프레임워크

Mock: 진짜 객체와 비슷하게 동작하지만 우리가 컨트롤할 수 있는 객체

 

  • 어떻게 활용: 외부 API, DB를 사용하는 경우 DAO, repository 객체를 mock 으로 만들고 mockito 를 사용해 동작 방식을 코딩 하고 테스트를 한다.
  • 장점: 외부 환경으로부터 독립적으로 테스트 가능
  • 테스트에 대한 의견: 내가 이미 구현한 클래스는 mocking 할 필요는 없다. 하지만 외부 서비스는 mocking 을 하는 게 좋겠다.
  • Mocking 하기 좋은 경우: 인터페이스를 사용해 개발하는 경우. 구현체를 mocking하면 좋다.
  • Stubbing: Mock 객체의 행동을 조작하는 것

의존성 주입하기

  • spring-boot-starter-test 가 mockito 를 알아서 주입해준다.
  • 스프링부트가 아니면, 다음을 추가한다.
<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 객체 생성하는 법

준비

StudyService는 생성자로 인터페이스를 받는다.

public class StudyService {
    private final MemberService memberService;
    private final StudyRepository repository;

    public StudyService(MemberService memberService, StudyRepository repository) {
        ...
    }
}

StudyService를 테스트 하기 위해서는 MemberService, StudyRepository 인터페이스를 Mock 객체로 생성해야 한다.

① Mock 객체 생성: 메소드로

@Test
void createStudyService() {
    MemberService memberService = Mockito.mock(MemberService.class);
    StudyRepository studyRepository = Mockito.mock(StudyRepository.class);

    StudyService studyService = new StudyService(memberService, studyRepository);
    assertNotNull(studyService);
}

MemberService, StudyRepository 인터페이스를 직접 구현하는 일을 mockito.mock 이 대신 해준다.

② Mock 객체 생성: 인스턴스 변수로

@ExtendWith(MockitoExtension.class) 
class StudyServiceTest {
    @Mock
    MemberService memberService;

    @Mock
    StudyRepository studyRepository;

    @Test
    void createStudyService() {
        StudyService studyService = new StudyService(memberService, studyRepository);
        assertNotNull(studyService);
    }
}
  • @Mock : Mockito.mock(MemberService.class) 을 간단하게 애노테이션으로 만들 수 있다.
  • @ExtendWith(MockitoExtension.class) : @Mock 애노테이션을 처리해주는 확장 모델이다.

③ Mock 객체 생성: 메소드 파라미터로

@ExtendWith(MockitoExtension.class) 
class StudyServiceTest {
    @Test
    void createStudyService(@Mock MemberService memberService, @Mock StudyRepository studyRepository) {
        StudyService studyService = new StudyService(memberService, studyRepository);
        assertNotNull(studyService);
    }
}

@ExtendWith(MockitoExtension.class)는 붙여줘야한다.


Stubbing 하는 법

기본적으로 모든 Mock 객체는

  • 리턴 타입이 객체 이면, null 을 리턴한다.
  • 리턴 타입이 Optional 이면, Optional.empty() 를 리턴한다.
  • 리턴 타입이 primitive 타입이면, 기본 Primitive 값 을 리턴한다. (int 는 0, boolean은 false)
  • 리턴 타입이 콜렉션 이면, 비어있는 콜렉션 을 리턴한다.
  • 리턴 타입이 void 면, 예외를 던지지 않고 아무런 일도 발생하지 않는다.

이런 특성을 가진 Mock 객체를 우리가 원하는 것을 리턴하도록 조작하는 것을 Stubbing 이라고 한다.

① return 있는 메소드 stubbing 하기

when([리턴있는메소드]).thenReturn([리턴값])

when([리턴있는메소드]).thenThrow([익셉션])

@Test
@DisplayName("return 있는 메소드 stubbing 하기")
void stubReturnMethod(@Mock MemberService memberService) {
    String email = "catsarah3333@gmail.com";
    Member member = new Member(1L, email);

    // stubbing
    when(memberService.findById(1L)).thenReturn(Optional.of(member)); // memberId가 1L 이어야지만 member 를 반환한다.
    when(memberService.findById(any())).thenReturn(Optional.of(member)); // ArgumentMatchers.any(): memberId가 뭐든간에 member 를 반환한다.
    when(memberService.findById(-1L)).thenThrow(new RuntimeException()); // 인자가 -1L이면 RuntimeException을 던져라.

    assertEquals(email, memberService.findById(1L).get().getEmail());
}

② return 없는 메소드 예외 발생 stubbing 하기

doThrow([익셉션]).when([클래스]).[리턴없는메소드명]

@Test
@DisplayName("return 없는 메소드 stubbing 하기")
void stubVoidMethod(@Mock MemberService memberService) {
    // stubbing
    // void 메소드에서 예외 던지기
    doThrow(new RuntimeException()).when(memberService).validate(-10L);

    assertThrows(RuntimeException.class, () -> memberService.validate(-10L));
}

③ 동일한 파라미터로 여러 번 호출될 때마다 각기 다르게 return 하도록 stubbing 하기

@Test
@DisplayName("동일한 파라미터로 여러 번 호출될 때마다 각기 다르게 return 하도록 stubbing 하기")
void stubDifferentReturnMethod(@Mock MemberService memberService) {
    String email = "catsarah3333@gmail.com";
    Member member = new Member(1L, email);
    // stubbing
    when(memberService.findById(any()))
            .thenReturn(Optional.of(member)) // findById() 1번째 호출: Optional<Member> 반환
            .thenThrow(RuntimeException.class) // findById() 2번째 호출: RuntimeException 던짐
            .thenReturn(Optional.empty()) // findById() 3번째 호출: Optional.empty() 반환
            ;

    assertEquals(email, memberService.findById(1L).get().getEmail());
    assertThrows(RuntimeException.class, () -> memberService.findById(3L));
    assertEquals(Optional.empty(), memberService.findById(10L));
}

• thenReturn(), thenThrow() 를 메소드 체이닝 해준다.


Mock 메소드 호출 검증 하는 법 (verifying)

① 메소드가 몇 번 호출 됐는지 검증

verify([mock 객체], [호출 돼야 하는 횟수]).[mock 객체 메소드]

@Test
@DisplayName("메소드 호출 횟수 검증")
public void verifyCallCnt(@Mock MemberService memberService) {
    Study study = new Study(10, "테스트");

    verify(memberService, never()).notify(study); // memberService.notify() 한 번도 호출 안 돼야함

    memberService.notify(study);
    verify(memberService, times(1)).notify(study); // memberService.notify() 한 번 호출 돼야함
}
  • notify(): 내가 만든 메소드명이다.

② 메소드 호출 순서 검증

InOrder inOrder = inOrder([mock객체]) inOrder.verify([mock객체]).[메소드명]

@Test
@DisplayName("메소드 호출 순서 검증")
public void verifyCallOrder(@Mock MemberService memberService) {
        // given
    Study study = new Study(10, "테스트");
    Member member = new Member(1L, "catsarah3333@gmail.com");

    // when
    memberService.notify(study);
    memberService.notify(member);

    // then
    InOrder inOrder = inOrder(memberService);
    inOrder.verify(memberService).notify(study); // memberService.notify(study) 호출 다음에
    inOrder.verify(memberService).notify(member); // memberService.notify(member) 호출 돼야함
}

③ 더 이상 검증할 호출 메소드가 없는지 체크

verifyNoMoreInteractions([mock객체])

테스트 성공인 경우

@Test
@DisplayName("성공 - 더 이상 검증할 메소드가 없는지 체크")
public void verifyNoMoreCall(@Mock MemberService memberService) {
    Study study = new Study(10, "테스트");

    memberService.notify(study);
    verify(memberService, times(1)).notify(study);
    verifyNoMoreInteractions(memberService);
}

memberService.notify(study) 가 호출되었고 이를 verify() 로 검증했으니 더 이상 검증할 memberService 호출 메소드가 없으므로 테스트 성공

테스트 실패인 경우

@Test
@DisplayName("실패 - 검증할 메소드 존재")
public void verifyNoMoreCall(@Mock MemberService memberService) {
    Study study = new Study(10, "테스트");

    memberService.notify(study);
//  verify(memberService, times(1)).notify(study);
    verifyNoMoreInteractions(memberService);
}

memberService.notify(study) 를 호출 했으나 verify() 로 검증 안 됐으므로 테스트 실패. = verify 할 interaction 이 존재하므로 실패

검증하지 않은 인터렉션 존재


BDD 스타일로 Mockito 코드 변경하는 법

BDD(Behavior Driven Development)

  • 메소드가 어떻게 행동해야하는지를 중심으로 작성하는 기법
  • 인자 테스트 코드 작성시 기준: Given / When / Then

BDDMockito.given([stubbing할메소드]).willReturn([리턴값])

BDDMockito.then([mock객체]).should([호출횟수]).[메소드명]

BDDMockito.then([mock객체]).shouldHaveNoMoreInteractions()

Mockito 코드를 Given / When / Then 스타일에 맞춰 작성할 수 있다.

@Test
@DisplayName("mockito 를 BDD style로 변경하기")
public void bddMockito(@Mock MemberService memberService, @Mock StudyRepository studyRepository) {
    // given
    Study study = new Study(10, "테스트");
    Member member = new Member(1L, "gmail.com");

    BDDMockito.given(memberService.findById(1L)).willReturn(Optional.of(member));
//  = Mockito.when(memberService.findById(1L)).thenReturn(Optional.of(member));

    BDDMockito.given(studyRepository.save(study)).willReturn(study);
//  = Mockito.when(studyRepository.save(study)).thenReturn(study);

    // when
    memberService.notify(study);

    // then
    BDDMockito.then(memberService).should(times(1)).notify(study);
//  = Mockito.verify(memberService, times(1)).notify(study);

    BDDMockito.then(memberService).shouldHaveNoMoreInteractions();
//  = Mockito.verifyNoMoreInteractions(memberService);
}

기존에 작성했던 stubbing 의 Mockito.when() 절은 BDD 에선 given 에 해당하므로 명세가 어울리지 않는다. 이 때 BDDMockito 객체를 사용해 given 절로 변환한다.

 

Mockito 설명

 

글 읽어주셔서 언제나 감사합니다. 좋은 피드백, 개선 피드백 너무나도 환영합니다.

 

연관글

https://elsboo.tistory.com/20 

 

Junit5 Assertions / Assumptions 설명

이 글은 온라인 강의를 듣고 직접 실습해보며 정리한 글입니다. 더 자세한 내용은 아래 강의에서 확인할 수 있습니다. 더 자바, 애플리케이션을 테스트하는 다양한 방법 - 인프런 | 강의 (백기선)

elsboo.tistory.com

https://elsboo.tistory.com/21

 

Junit5 애노테이션 설명

이 글은 온라인 강의를 듣고 해당 내용을 직접 실습해보며 정리한 글입니다. 더 자세한 내용은 아래 강의에서 확인할 수 있습니다. 더 자바, 애플리케이션을 테스트하는 다양한 방법 - 인프런 |

elsboo.tistory.com