freeParksey
밥세공기
freeParksey
전체 방문자
오늘
어제
  • 분류 전체보기 (150)
    • JAVA (32)
      • 자바스터디 (21)
      • Java in action (6)
      • OOP (1)
      • test (2)
    • 알고리즘 문제 (51)
      • 백준 (49)
    • C (Atmega128) (7)
    • 인공지능 (11)
    • 운영체제 (8)
    • 디자인패턴 (5)
    • 잡다한것 (2)
    • 사용기 (3)
      • 도커 (3)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 알고리즘
  • dto 변환
  • Iterator
  • 분류
  • 백트랙킹
  • Thread 동작
  • 그리드
  • 백준
  • 집합과 맵
  • 3주차
  • Thread #JVM #자바스터디 #
  • Collection
  • 동적계획법
  • 스트림
  • 백트래킹
  • 프리코스
  • Python
  • dto 변환 위치
  • 딥러닝
  • 후기
  • 동작 파라미터화
  • 운영체제
  • 우테코
  • java
  • 자바스터디
  • generic
  • 우아한테크코스
  • 상속
  • 자바
  • 재귀기초

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
freeParksey

밥세공기

Mockito 간단 정리
JAVA/test

Mockito 간단 정리

2023. 8. 4. 02:17

Mockito란?

Mockito는 자바에서 테스트용으로 사용되는 Mocking 프레임워크입니다.

 

여러 Mock객체를 대신 간편하게 만들어 줘서 사용할 수 있습니다.

대체로 A라는 클래스가 여러 의존성을 가지고 있을 때 A에 대한 클래스만 집중하여 테스트 하고 싶을 때 다른 의존성들을 전부 Mock으로 변환시켜 사용하게 됩니다. (결국 클래스 단위 테스트를 진행하고자 할 때 사용하게 됩니다.)

 

Mockito를 사용하기 위해선 외부 라이브러리인 만큼 주입을 해줘야 겠죠

 

Mockito Dependency

// Gradke
implementation 'org.mockito:mockito-core:5.4.0'

// Maven
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
</dependency>

 

 

MockitoExtension.class vs MockitoAnnotations.openMocks()

 

먼저 Mockito를 사용하기 위해서는 mock을 사용한다고 알려줘야하는데 2가지 방법이 있습니다.

 

@BeforeEach
void initInjection() {
    MockitoAnnotations.openMocks(this);
}

이렇게 하는 경우에는 해당 클래스에서 Mock을 사용한 다는 것을 알려주는 것입니다.

 

또 다른 방법으로는

@ExtendWith(MockitoExtension.class)

ExtendWith 어노테이션을 붙여주는 것인데 이것이 가능한 이유는

 

ExtendWith에서 이러한 API를 상속받는 클래스에 대해 자동으로 적용을 해주는데

 

Mockito가 이러한 기능을 사용할 수 있도록 `MockitoExtension`이라는 클래스를 지원해 주기 때문이다.

 

 

Mock 주입

Mock을 주입하는 방식에 따라 나눌 수 있는데, `@Mock`, `@Spy`, `@InjectMocks`가 있습니다.

 

 

@Mock

말 그대로 mock객체를 만들어 주는 어노테이션이다.

 

List mockList = Mockito.mock(ArrayList.class);

// or field로
@Mock
private CustomMock customMock;

내부적으로 메서드를 invoke하여 메서드들의 정보만 가져온다.

 

따라서 실제로 메서드를 실행 시킬 수 있지만, 동작은 하지 않는다.

 

 

아래와 같은 CustomMock객체가 있다고 해보자

@Component
public class CustomMock {

    private static final String staticField = "Static field";
    private String field = "instance field";

    public CustomMock() {
        System.out.println("[Custom Mock]");
        System.out.println(staticField);
        System.out.println(field);
    }

    public void run() {
        int a = getInt();
        System.out.println("Custom Mock is run : "+a);
        System.out.println(test());
    }

    public int test() {
        return 1000;
    }

    private int getInt() {
        return 100;
    }
}

 

이것을 mocking해서 값을 봐보면 내부에는 field가 null로 되어 있고 나머지는 존재하지 않는 것을 볼 수 있다.

즉 내부적으로 어떠한 값고 가질 수 없는 상태인 것이다.

 

@Test
void mockTest() {
    Integer result = customMock.test();
    verify(customMock).test();
    log.info("반환 값은? : {}", result);
}

따라서 아래와 같이 동작하게 되면, 

와 같이 반환 값이 0인 것을 볼 수 있다.

결국 CustomMock에 대한 생성자를 호출하지 않은 것을 볼 수 있고,

또한 `verify`를 사용했는데, `verfiy`의 경우 customMock이 특정 메서드를 실행했는지 확인하는 메서드이며, 만약 

@Test
void mockTest() {
    Integer result = customMock.test();
    verify(customMock).run();
    log.info("반환 값은? : {}", result);
}

위와 같이 실행하지 않는 메서드를 확인하게 되면

실패하게 된다.

 

결과적으로, `Mock`은 똑같은 객체를 생성하게 되지만, 내부적으로 동작만 가능할 뿐 다른 추가적인 작업이 불가능 하다 볼 수 있다.

 

@Spy

실제 메서드 들을 invoke하는데, 실제 인스턴스를 가져온다 생각하면 될 것 같습니다.

 

@Component
public class CustomSpy {

    private static final String staticField = "Static field";
    private String field = "instance field";

    public CustomSpy() {
        System.out.println("[Custom Spy]");
        System.out.println(staticField);
        System.out.println(field);
    }

    public void run() {
        int a = getInt();
        System.out.println("Custom Spy Mock is run : " + a);
        System.out.println(test());
    }

    public int test() {
        return 1000;
    }

    private int getInt() {
        return 100;
    }
}
@Test
void spyTest() {
    customSpy.run();
    verify(customSpy).test();

    given(customSpy.test()).willReturn(400);
    customSpy.run();
}

위 코드를 실행하게 되면

위와 같이 생성자가 정상적으로 실행 되는 것을 볼 수 있고, 내부적으로는 field값이 생성되어 있는 것을 볼 수 있습니다.

 

또 다른 특징으로는 내부적으로 메서드가 invoke로 실제 메서드를 동작하게 하더라도 값을 변경할 수가 있고 아래와 같은 결과를 확인할 수 있습니다.

여기서 주의해야할 점은`Spy`의 경우 내부적으로 추가적인 mock객체를 넣어줄 수 없다는 점이 있습니다.

 

 

@InjectMock

`InjectMock`의 경우 의존성을 주입해 주는 어노테이션 입니다. 따라서 Mock객체를 생성하는 `Spy`나 `Mock` 어노테이션과는 다르다는 것을 알 수 있습니다.

근데 생각해보면 우리는 지금까지 @InjectMock만 붙여줬지 다른 어노테이션을 붙이지는 않았는데, 위에서 설명드렸던 것 처럼 생성자 주입만 하기때문에 에초에 Mock인스턴스를 만들지 않아서 만들어 지지 않아야 정상입니다.

 

이건 사실 @InjectMock의 인스턴스가 초기화 되기 전에 실행되면 자동으로 생성자를 호출해주게 됩니다.

 

추가적인 내용이나 생성자의 유무와 동작에 따른 원리를 알고 싶으시면 `@InjectMocks`를 확인해 보시면 됩니다.

 

 

@InjectMock을 통한 예시를 보여 드리면

@Service
public class CustomService {
    private final CustomInject injectMock;
    private final CustomSpy customSpy;
    private final CustomMock customMock;

    public CustomService(CustomInject customInjectMock, CustomSpy customSpy, CustomMock customMock) {
        System.out.println("[Custom Service]");
        this.injectMock = customInjectMock;
        this.customMock = customMock;
        this.customSpy = customSpy;
    }

    public void run() {
        int a = getInt();
        System.out.println(injectMock == null);
        System.out.println(a);
        System.out.println(test());
    }

    public int test() {
        return 1000;
    }


    private int getInt() {
        return 100;
    }
}

따라서 run을 실행하게 되면

각 객체가 정상적으로 인스턴스가 생성되고 값을 주입 받은 것을 확인할 수 있고

와 같은 결과가 나옵니다.

@Test
void injectTest() {
    customService.run();
    customInjectMock.run();

    verify(customService).test(); // 에러
    verify(customInjectMock).test(); // 에러
}

다만 여기서 verify는 error가 발생하는데, 실제 인스턴스를 주입하는 InjectMock은 mock이 아니기 때문에 사용할 수 없습니다.

 

 

 

추가

 

여러 에러를 한번에 확인

각각의 에러를 확인하기 위해 `assertj`를 사용하게 되면 처음 만난 에러에 멈추는 경우가 있습니다. 여러 값들을 비교 했을 경우 한번에 확인하여 고치고 싶은데 못하는 경우가 발생하죠.

 

이러한 문제를 피할 수 있는 방법으로  `junit`의 `assertAll`이 있습니다.

따라서 위에서 있던 2개의 verify에러같은 경우에도

assertAll("Inject test",
    () -> {
        verify(customService).test();
        verify(customInjectMock).test();
    }
);
---
assertAll(
    () -> verify(customService).test(),
    () -> verify(customInjectMock).test()
);

위와 같이 2가지 방식으로 해결할 수 있습니다. 처음 같은 경우에는 에러에 대한 Display name을 지정해 주는 것이고 아래는 그냥 출력해 주는 방식 입니다.

 

@Capture

Capture라는 어노테이션도 존재하는데, Capture의 경우 이전에 실행 했던 메서드에서 인자로 전달한 값을 그대로 가져와서 사용하기 위함입니다.

 

예를들어 인자로 받은 값을 그대로 넘겨주는 메서드를 작성한다고 하자

public int test2(int a) {
    return a;
}

와 같은 코드가 있다면

 

int result = customSpy.test2(100);
verify(customSpy).test2(captor.capture());
assertThat(result).isEqualTo(captor.getValue());

위와 같이 코드를 작성하게 되면 `result = 100`이고 captor.getValue()또한 100 이므로 테스트가 통과하게 된다.

'JAVA > test' 카테고리의 다른 글

Java - 시간을 다루기 및 테스트  (2) 2023.08.06
    'JAVA/test' 카테고리의 다른 글
    • Java - 시간을 다루기 및 테스트
    freeParksey
    freeParksey
    Github: https://github.com/parksey

    티스토리툴바