용로그
article thumbnail
Published 2023. 4. 17. 16:32
[Test] Mockito 사용법 Java

이번 글에서는 Mockito에 관련해서 글을 써보려 합니다. Spring이 적용된 Mockito가 아닌 JUnit으로만 사용할 수 있는 수준의 문법을 정리합니다.

 

Mockito Test Framwork가 처음 사용하는 분들에게 불친절하다고 생각합니다. 저도 그래서 다른 크루들과 함께 공부했고 이에 대한 코드는 다음 레파지토리에 정리해두었습니다.

 

https://github.com/wonyongChoi05/mockito-study

 

GitHub - wonyongChoi05/mockito-study: 우아한테크코스 프롤로그 Mockito + JUnit을 이용한 테스트 작성 방식

우아한테크코스 프롤로그 Mockito + JUnit을 이용한 테스트 작성 방식 문서화. Contribute to wonyongChoi05/mockito-study development by creating an account on GitHub.

github.com

 

Mockito란?


Mock 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 프레임워크

용어 정리

Mock

  • 진짜 객체와 비슷하지만 물리적으로 같지 않고 프로그래머가 직접 행동을 관리하는 객체이다.
  • Mock은 모든 상호작용을 기억한다. 사용자는 Mock의 어떤 메서드가 실행되었는지 선택적으로 검증할 수 있다.

Stubbing

  • 테스트 코드에서 Mock 객체를 사용할 때, Mock의 특정 메서드 호출응답을 정의하는 것을 말한다.

테스트를 작성하는 자바 개발자의 약 45%(JetBrains 설문조사)가 Mockito Framework를 사용한다. Mockito를 대체할 수 있는 테스트 프레임워크는 EasyMock, JMock 등이 있다.

 

왜 사용하는가?


애플리케이션에서 구현되지 않은 객체 또는 메서드를 테스트 하려고 할 때, 해당 클래스가 어떻게 동작하는지 항상 미리 생각하고 계산하며 테스트를 작성한다면 매우 불편할 것이다.

 

이럴때 미리 Mock 객체를 생성해서 사용하면 조금 더 편하게 테스트를 할 수 있다.

ex) Member 또는 MemberDao가 어떻게 작동하는지 Mockito를 이용해 만들어 놓으면 Member와 MemberDao 객체를 구현하기 전에도 테스트를 작성할 수 있다.

 

아직도 감이 잘 안올 것이다. 아래에서 코드와 함께 보면서 사용법과 개념을 익혀보자.

 

이미 구현된 객체와 메서드를 Mocking할 필요가 있을까?
굳이 그럴 필요는 없다. 하지만 개발자가 컨트롤하기 힘든 부분이나 아직 구현되지 않은 클래스는 모킹이 필요한 경우가 많다.

 

사용하기 전


  • 스프링부트 2.2+ 프로젝트 생성시 spring-boot-start-test에서 자동으로 Mockito를 추가해준다.
  • 스프링부트 2.2 버전 이하 또는 스프링부트를 사용하지 않는다면 의존성을 추가해준다.

이 글에서는 스프링부트를 사용하지 않고 Only JUnit으로만 테스트를 작성하기 때문에 아래와 같은 의존성을 추가해준다.

 

Gradle Dependency 추가

testImplementation 'org.mockito:mockito-junit-jupiter:5.2.0'

 

본격적으로 알아보기 전에


본격적으로 Mockito를 알아보기 전에 앞으로 설명할 코드엔 간간히 verify와 when-then이라는 문법이 보일 것이다. 만약 자신이 mockito를 처음 접하거나 위에 대한 문법을 모른다면 다음 코드에서 간단히 학습하고 오자.

verify라는 문법 또한 mock 객체에 대한 검증이기 때문에 mock에 대한 문법이 나올 수 있다. mock에 대한 문법은 아래에서 더 자세히 살펴볼것이기 때문에 지금은 verify라는 문법에 대해서 "아~ 이런 느낌이구나" 정도만 알고 넘어가자.

 

Mockito Annotation


@Mock

  • Mock 객체의 인스턴스 내부는 비어있다. (Null)
  • Mock 객체를 만드는 방법은 다음과 같다.

어노테이션 사용

@Mock
List<String> mockedList;

직접 선언

List<String> mockedList = mock(List.class);

 

@Mock
List<String> mockedList;

@DisplayName("목 객체를 stubbing 하여 테스트한다. (@Mock 사용)")
@Test
public void useMockAnnotation() {
    //given
    when(mockedList.add("one")).thenReturn(false);
		
    //when
    final boolean result = mockedList.add("one");

    //then
    verify(mockedList).add("one");
    Assertions.assertThat(result).isFalse();
}

 

@Spy

  • Spy 객체는 기존 인스턴스와 동일하다.
  • 하지만 원하는 부분에 대해서만 stubbing을 할 수 있다.
  • Spy 객체를 만드는 방법은 다음과 같다.

어노테이션 사용

@Spy
List<String> spyList;

직접 선언

List<String> spyList = Mockito.spy(List.class);

 

@Spy
List<String> spyList;

@DisplayName("Spy한 객체를 테스트한다. ( @Spy 사용 )")
@Test
public void useMockAnnotation() {
    //given
    when(spyList.add("one")).thenReturn(false);
		
    //when
    final boolean stubResult = spyList.add("one");
    final boolean originalResult = spyList.add("two");
		
    //then
    //메서드 호출을 검증한다.
    verify(spyList).add("one");
    verify(spyList).add("two");

    Assertions.assertThat(stubResult).isFalse();
    Assertions.assertThat(originalResult).isTrue();
}

 

@Captor

  • 메서드에 전달된 인자를 캡쳐하는 기능을 제공한다.
  • 캡처할 인자의 타입에 해당하는 Captor 객체를 생성해야 한다.
  • Verify 메서드로 메서드의 인자를 캡처할 수 있다.
  • Mock 객체가 주입되어 내부적으로 객체의 메서드가 호출되는 경우에도 인자를 캡쳐할 수 있다.

어노테이션 사용

@Captor
final ArgumentCaptor<String> args;

직접 선언

ArgumentCaptor<List> listArgumentCaptor = ArgumentCaptor.forClass(List.class);

 

@Captor
final ArgumentCaptor<String> args;

@DisplayName("ArgumentCaptor를 사용하여 메서드 호출에 사용된 인자를 저장하여 검증한다.(호출을 한 번 했을 때)")
@Test
public void useArgumentCaptorOnce() {
    // given
    final List<String> mockList = mock(List.class);

    // when
    mockList.add("first");
    verify(mockList).add(args.capture());

    // then
    Assertions.assertThat(args.getValue()).isEqualTo("first");
}

 

@InjectMock

  • 해당 객체의 멤버 변수로 존재하는 의존된 다른 객체들이
  • Mock혹은 Spy로 생성된 객체라면 의존성 주입을 해주는 기능을 제공한다.

 

  • Name이라는 객체를 컴포지션 관계로 가지고 있는 Car 객체다.
public class Car {

    private final Name name;

    public Car(Name name) {
        this.name = name;
    }

    public boolean checkCar(String name) {
        return this.name.isEqualsName(name);
    }

}

 

  • field로 가지고 있는 name과 파라미터로 받은 name이 같은지 검증하는 메서드를 가지고 있다.
public class Name {

    private final String name;

    public Name(String name) {
        this.name = name;
    }

    public boolean isEqualsName(String name) {
        return this.name.equals(name);
    }

}

 

  • InjectMocks 어노테이션을 사용하지 않는다면 아래와 같이 Mock 객체를 직접 생성자에 넣어 Mock 객체가 주입된 Car 객체를 만들어야 한다.
@Mock
private Name name;

@DisplayName("InjectMocks를 사용하지 않고 Mock 의존성을 주입받는 방법")
@Test
void non_inject_mocks() {
    // given
    final Car car = new Car(name);

    // when
    Mockito.when(car.checkCar("hyundai")).thenReturn(true);

    // then
    Assertions.assertThat(car.checkCar("hyundai")).isTrue();
    Assertions.assertThat(car.checkCar("kia")).isFalse();
}

 

  • InjectMocks 어노테이션을 사용한다면 따로 Car를 생성할 때 의존성을 주입하지 않아도 Mock이나 Spy 객체로 생성된 객체가 있다면 의존성을 주입해 준다.
@Mock
private Name name;

@InjectMocks
private Car car;

@DisplayName("InjectMocks를 사용해서 Mock 의존성을 주입받는 방법")
@Test
void inject_mocks() {
    // when
    Mockito.when(car.checkCar("hyundai")).thenReturn(true);

    // then
    Assertions.assertThat(car.checkCar("hyundai")).isTrue();
    Assertions.assertThat(car.checkCar("kia")).isFalse();
}

 

Mocking Exception


반환 값이 존재할 때 Mocking Exception 처리하기

반환 값이 존재한다는게 무슨 의미일까? 말 그대로 when 구문의 메서드가 반환 값이 있다면 아래와 같은 문법으로 처리가 가능하다는 것이다. 메서드 실행시 Exception이 발생하도록 stubbing 할 수 있다.

@DisplayName("반환값이 존재하는 메서드를 Exception Stubbing 할 때 - class")
@Test
void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown() {
    List<String> strList = mock(List.class);
    when(strList.size()).thenThrow(CustomException.class);
    assertThatThrownBy(strList::size)
            .isInstanceOf(CustomException.class);
}

 

반환 값이 존재하지 않을 때 Mocking Exception 처리하기

하지만 반환 값이 존재하지 않는다면 위와 같이 처리해서는 안된다. 반환 값이 없는 void 타입의 메서드라면 다음과 같이 처리해보자.

@DisplayName("반환값이 존재하지 않는 메서드를 Exception Stubbing 할 때 - class")
@Test
void whenConfigVoidReturnMethodToThrowEx_thenExIsThrown() {
    List<String> strList = mock(List.class);
    doThrow(CustomException.class).when(strList)
            .clear();
    assertThatThrownBy(strList::clear)
            .isInstanceOf(CustomException.class);
}

 

예외 객체 넘기기

  • 이전에 사용했던 문법들에서 예외 객체 자체를 넘길 수 있다.
  • 예외 객체를 생성해 넘김으로써 에러 메시지를 정의해줄 수 있다.
@DisplayName("반환값이 존재하는 메서드를 Exception Stubbing 할 때 - object")
@Test
void whenConfigNonVoidReturnMethodToThrowExWithNewExObj_thenExIsThrown() {
    List<String> strList = mock(List.class);
    when(strList.size()).thenThrow(new CustomException(EXCEPTION_MESSAGE));
    assertThatThrownBy(strList::size)
            .isInstanceOf(CustomException.class)
            .hasMessage(EXCEPTION_MESSAGE);
}

@DisplayName("반환값이 존재하지 않는 메서드를 Exception Stubbing 할 때 - object")
@Test
void whenConfigVoidReturnMethodToThrowExWithNewExObj_thenExIsThrown() {
    List<String> strList = mock(List.class);
    doThrow(new CustomException(EXCEPTION_MESSAGE)).when(strList)
            .clear();
    assertThatThrownBy(strList::clear)
            .isInstanceOf(CustomException.class)
            .hasMessage(EXCEPTION_MESSAGE);
}

 

Mockito ArgumentMatcher


  • mock 메서드를 다양한 방식으로 설정할 수 있다.
  • 가장 간단하게 메서드 파라미터에 상수를 넣어 mock 메서드를 설정할 수 있다.
doReturn(STUBED_VALUE).when(mockedList).get(0);

 

하지만 위의 방법은 단지 mockedList에서 0번째 값을 꺼내려고 할 때만 해당되는 stubbing이다. 우리는 좀 더 넓은 범위의 파라미터를 정의하거나 일반적인 상황에 대해 고려할 필요가 있다.

 

이 때 Argumentmatcher를 사용해 위와 같은 문제를 해결할 수 있다.

 doReturn(STUBED_VALUE).when(mockedList).get(anyInt());

 

get 부분에 특정 인덱스가 아닌 ArgumentMathcer.anyInt() 함수를 사용한다면, 전달되는 Int 타입의 모든 파라미터의 메서드를 정의할 수 있다.

 

만약 STUBED_VALUE3이 들어간다면, mockedList에서 몇 번째 인덱스를 꺼내더라도 3이 꺼내질 것이다. 따라서 ArgumentMatcher는 하나의 값이 아닌 여러 개의 값에 대한 유연한 검증이 필요할 때 사용된다.

profile

용로그

@용로그

벨덩보단 용덩 github.com/wonyongChoi05