Mockito

김아무개·2023년 5월 13일
0

Spring Boot 🍃

목록 보기
10/95
post-custom-banner

컨셉이 정말 재밌는 프레임 워크다 😆


모킹?목킹?마킹?


맛있는 모킹 프레임워크!

사이트에 가보면 숙취 없는 맛이 정말 좋은 모킹 프레임워크라고 소개하고있다.
🙈🍸

근데 모킹이 뭐지;

모르는게 너무 많다 ㅋㅋ 🙈


모킹 Mocking

세상에 🙊
조롱이란 뜻이었다.
디비를 조롱하는건가.........??!?!?

놀라워서 앵무새한테 번역을 부탁해봤더니 흉내내는 이라는 뜻도 있었다.
여기서 사용된 모킹은 흉내내는 이라는 의미겠지!


Mock Object 모의 객체

모의 객체 Mock Object란

주로 객체 지향 프로그래밍으로 개발한 프로그램을 테스트 할 경우

테스트를 수행 할 모듈과 연결되는 외부의 다른 서비스나 모듈들을
실제 사용하는 모듈을 사용하지 않고
실제의 모듈을 흉내내는 가짜 모듈을 작성하여 테스트의 효용성을 높이는데 사용하는 객체이다.

사용자 인터페이스나 데이터베이스 테스트 등과 같이
자동화된 테스트를 수행하기 어려운 때 사용된다.

출처 : 위키백과




Mockito 메서드


mock()

모의 객체를 생성.

예를 들면
List mockedList = mock(); 이라고 작성할 경우
List 자료형인 Mock 객체를 만들어준다.



when().thenReturn()

목 객체의 메서드가 호출될 때 반환될 값을 지정.

예를 들면
when(list.get(0)).thenReturn("하나"); 라고 작성할 경우
list.get(0) 호출 시 하나를 반환!



given().willReturn()

BDD 스타일 ( Behavior Driven Development )의
테스트를 지원하기 위해 제공하는 메서드이다.

BDD 스타일은
테스트 케이스를 주어진 상황given에서 ,
어떤 행동을 할 때 when ,
기대하는 결과는 이렇다 then라는 형식으로 작성하는것을 권장한다.

Mockito에서 given()은
주로 when() 대신 사용되며, 동일한 기능을 수행하지만
테스트 케이스를 읽고 이해하기 더 쉽다.

예를 들면
given(list.get(0)).willReturn("하나"); 라고 작성할 경우
list.get(0) 호출 시 하나를 반환!



verify()

특정 메서드가 호출 되었는지 검증.

예를 들면
verify(list).get(0) 이라고 작성할 경우
list.get(0)이 1번 호출 되었는지에 대해 검증한다.

  • 호출 횟수 지정 : times(int 횟수) 메서드 사용!
    ex )
    verify(list, times(10)).get(0) : list.get(0) 이 10번 호출되었는지 검증

작성하지 않은 동작을 검증하려고 하면 아래와 같이 에러가 난다.

"Comparison Failure: <클릭하여 차이점 확인>" 누르면 자세히 알려줌!
요렇게👇



Argument matchers

메서드 호출 시 전달되는 인자의 타입을 지정할 수 있게 해주는 기능이다.
Argument matchers에 해당하는 주요 메서드는 다음과 같다.

anyInt(), anyLong(), anyDouble(), anyString(), any() 등

각각 임의의 int, long, double, String, Object에 매칭

사용 예시

// 값 생성
given(list.get(0)).willReturn("one");
given(list.get(1)).willReturn("two");
given(list.get(anyInt())).willReturn("🙈");

// 값 확인
System.out.println(list.get(0));
System.out.println(list.get(1));

eq()

특정 값과 일치하는 인자에 매칭.

argThat()

주어진 조건을 만족하는 인자에 매칭

사용 예시

@Test
void listTest2() {
    // 값 생성
    given(
            list.get(
                    Optional.ofNullable(
                            argThat(new ArgumentMatcher<Integer>() {
                                @Override
                                public boolean matches(Integer argument) {
                                    return argument >= 5;
                                }
                            })
                    ).orElse(0)
            )
    ).willReturn("🙈");

    // 값 확인
    System.out.println(list.get(5));
    System.out.println(list.get(1));
    System.out.println(list.get(123));
}

argThat을 Optional.ofNullable로 감싼 이유는 자꾸 null이 나와서이다. ㅠㅠ
왜지.. 왜 null이 나올까..... 원인을 알아내지 못함 ㅠㅠ

anyListOf(Class<T>), anySetOf(Class<T>), anyMapOf(Class<K>, Class<V>) 등

각각 임의의 List<T>, Set<T>, Map<K, V>에 매칭


spy()

실제 객체를 감싸는 모의 객체를 생성.
이를 partial mocking이라고도 함!

실제 객체의 일부 메소드만 모의화 하거나 오버라이드 하려는 경우에 유용하다.

spy를 사용하면 실제 객체의 모든 메소드는 실제로 동작하지만,
필요한 경우 특정 메서드의 동작을 변경하거나
메서드 호출을 검증할 수 있다.

주의할 점은
when()과 같이 stubbing 할 때 실제 메소드가 호출된다는 것이다.

만약 stubbing 하려는 메서드가 사이드 이펙트를 가지거나
특정 조건에서만 동작한다면
willReturn().given() 구문을 사용하는 것이 좋다.

doReturn().when() 구문을 사용하면,
메서드가 실제로 호출되지 않는다.

사용 예시

// 실제 객체 생성
List<String> aList = new LinkedList<>();

// 실제 객체를 wrapping 하는 spy 객체 생성
List<String> spyList = spy(aList);

// 이 다음 문장인 add 안 될 경우 size()를 임의로 늘려주면 됨
// given(spyList.size()).willReturn(100);

// 실제 메서드 호출
spyList.add("하하");
spyList.add("우하하");

System.out.println(spyList.get(0)); // 하하
System.out.println(spyList.get(1)); // 우하하
System.out.println(spyList.size()); // 2

// spy 객체의 메서드 동작 변경
given(spyList.get(1)).willReturn("우하핳");
System.out.println(spyList.get(1)); // 우하핳

// doReturn 사용 시 get(0) 메서드는 실제로 호출되지 않음
willReturn("HaHa").given(spyList).get(0);
System.out.println(spyList.get(0)); // HaHa

verify(spyList, times(2)).get(0);
verify(spyList).size();


doReturn()...when():

willReturn()...get():

반환값이 없어 when().thenReturn()을 사용할 수 없는
void 메서드를 스텁하는 경우나

* 테스트 스텁( Test stub, 이하 Stub )이란 
테스트 중인 모듈이 호출하는 다른 소프트웨어 구성 요소(예: 모듈, 변수, 객체)를 
일시적으로 대체하는 소프트웨어 구성 요소를 말한다. 

정보 제공: https://blog.naver.com/suresofttech/221180956096

실제 메서드 호출을 회피하고 싶을 때 사용한다.
왜냐하면 메서드를 실제로 실행하지 않고 지정한 결과를 바로 반환하기 때문이다.

실제 메서드 호출에 따른 부작용을 피하고자 할 때 유용하다.



Verification modes

verify(mock, times(5)).someMethod("was called five times");와 같이 사용하여
특정 메서드가 특정 횟수만큼 호출되었는지 검증할 수 있다.

Verification modes에 해당하는 주요 메서드는 다음과 같다.

times(n)

특정 메서드가 n번 호출되었음을 검증한다.

verify(list, times(1)).get(0);

never()

특정 메서드가 한 번도 호출되지 않았음을 검증한다.

verify(list, never()).get(0);

atLeast(n)

특정 메서드가 최소 n번 호출되었음을 검증한다.

verify(list, atLeast(1)).get(0);

atMost(n)

특정 메서드가 최대 n번 호출되었음을 검증한다.

verify(list, atMost(1)).get(0);

only()

특정 메서드가 한 번 호출되었고,
다른 메소드는 호출되지 않았음을 검증한다.

verify(list, only()).get(0);

InOrder(n)

여러 메서드 호출이 특정 순서대로 이루어졌음을 검증한다.

사용 예시 1 : 검증 성공

// 비교할 Mock 객체 2개 생성
List<String> first = mock();
List<String> second = mock();

first.add("하이");
second.add("Hi");

InOrder inOrder = inOrder(first, second); // 순서를 검증할 목 객체 지정

inOrder.verify(first).add("하이");
inOrder.verify(second).add("Hi");

사용 예시 2 : 검증 실패

// 비교할 Mock 객체 2개 생성
List<String> first = mock();
List<String> second = mock();

first.add("하이");
second.add("Hi");

InOrder inOrder = inOrder(first, second); // 순서를 검증할 목 객체 지정

inOrder.verify(second).add("Hi");
inOrder.verify(first).add("하이");





Mockito 실습

db를 모방한 간단한 테스트 실습
인데 너무 어려웠던 것 ㅠㅠ

아직 repository / service 구조가 어색해서 더 어렵게 느껴졌던 것 같다.

dependency 추가

Mockito
Mockito-junit-jupiter

dependencies { 
    // mockito ----------------------------------------------------
    testImplementation("org.mockito:mockito-core:4.11.0+")
    testImplementation("org.mockito:mockito-junit-jupiter:4.11.0")

}

User / Repository / Service 자바 파일 작성

User

import lombok.*;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {
    private int id;
    private String name;
    private String email;
}

UserRepository

import java.util.Optional;

public interface UserRepository {
    Optional<User> findById(int id);
}

UserService

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(int id) {
        return userRepository
                .findById(id)
                .orElseThrow(
                        () -> new RuntimeException("User not found")
                );
    }
}

Test 파일 작성!

1. 뼈대 만들기

@DisplayName("Service Test")
@ExtendWith(MockitoExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
public class MockitoDBTest {
    @Mock // UserRepository 타입의 모의 객체 생성
    UserRepository userRepository;

    @InjectMocks // 모의 객체 주입
    UserService userService;

    @Test
    void insert() {
    }
}

@DisplayName("Service Test")

테스트 클래스에 붙이는 이름


@ExtendWith(MockitoExtension.class)

@ExtendWith : 사용자 정의 확장을 등록하는 어노테이션

MockitoExtension.class : Mockito 라이브러리에서 제공하는 JUnit 5 확장.

MockitoExtension.class를 사용하면
Mockito의 다양한 기능을 JUnit 5 테스트와 쉽게 통합할 수 있다.

MockitoExtension.class에서 제공하는
@Mock, @InjectMocks, @Spy 등의 어노테이션을 사용하여
Mockito의 모의 객체를 생성하고 주입할 수 있다.


@TestInstance(TestInstance.Lifecycle.PER_METHOD)

테스트 클래스의 생명주기를 설정하는 어노테이션이다.

TestInstance.Lifecycle.PER_METHOD

@TestInstance()의 기본 값은 TestInstance.Lifecycle.PER_METHOD로,
테스트 생명주기를 메서드 단위로 설정한다는 의미이다.

이 기본값으로 인해
테스트 클래스를 실행하면
클래스 안의 메서드들이 순서 없이 임의적으로 실행된다.

TestInstance.Lifecycle.PER_METHOD 설정을 유지하면( 기본값 )
@BeforAll , @AfterAll 어노테이션이 붙은 메서드를 static으로 작성해야 한다.
또한 메서드간에 테스트 인스턴스를 공유할 수 없어 ,
각각 안정적으로 독립적인 테스트가 가능해진다.

TestInstance.Lifecycle.PER_CLASS

TestInstance.Lifecycle.PER_CLASS 설정은
테스트 생명주기를 메서드 단위가 아닌, 클래스 단위로 설정하는 것이 된다.

이러한 설정을 한다면,
한 클래스 안의 메서드들이 테스트 인스턴스를 공유할 수 있게 되고,
@BeforeAll , @AfterAll 어노테이션이 붙은 메서드를 non-static으로 관리할 수 있다.

하지만,
테스트 인스턴스를 모든 메서드가 공유하게 된다면
테스트가 서로에게 영향을 미치게 되므로 테스트 독립성이 손상될 수 있기 때문에
주의해서 사용해야 한다.


@Mock

이 어노테이션이 붙은 객체를 모의 객체로 만들어준다.


@InjectMocks

이 어노테이션이 붙은 객체에 모의 객체를 주입해준다.


2. BDD Behavior Driven Development 작성!

@Test
void insert() {

    User user = new User(1, "몽키", "monkey@zh.kim");

    given( userRepository.findById(1) ).willReturn( Optional.of(user) );

    User result = userService.getUserById(1);

    assertEquals(user, result);
    verify( userRepository, times(1) ).findById(1);

    System.out.println(result.toString());
}

1. 입력할 데이터를 만든다

User user = new User(1, "몽키", "monkey@zh.kim");

2. 가상의 DB에 값을 저장하는 코드를 작성한다. (given)

given( userRepository.findById(1) ).willReturn( Optional.of(user) );

3. 가상의 DB에 저장한 값을 불러오는 코드를 작성한다. (when)

User result = userService.getUserById(1);

4. 입력할때 사용한 객체와 저장된 값을 불러온 객체를 비교한다. (then)

assertEquals(user, result);

5. 가상의 DB에 값을 입력하는 코드가 1회만 호출되었는지 검증한다.

verify( userRepository, times(1) ).findById(1);

전체 코드!

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.sql.*;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@DisplayName("Service Test")
@ExtendWith(MockitoExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_Method)
public class MockitoDBTest {
    @Mock // UserRepository 타입의 모의 객체 생성
    UserRepository userRepository;

    @InjectMocks // 모의 객체 주입
    UserService userService;

    @Test
    void insert() {
        User user = new User(1, "몽키", "monkey@zh.kim");

        given(userRepository.findById(1)).willReturn(Optional.of(user));

        User result = userService.getUserById(1);

        assertEquals(user, result);
        verify(userRepository, times(1)).findById(1);

        System.out.println(result.toString());
    }
    
    @Test
    void insert2() {
        User user = new User(2, "코알라", "native.bear@zh.kim");

        given(userRepository.findById(1)).willReturn(Optional.of(user));

        User result = userService.getUserById(1);

        assertEquals(user, result);
        verify(userRepository, times(1)).findById(1);

        System.out.println(result.toString());
    }

}
profile
Hello velog! 
post-custom-banner

0개의 댓글