ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 👻 Mockito
    ☕️ java 2023. 10. 31. 14:20

     

     

    우아한테크코스 프리코스의 경주 게임미션을 진행하면서 임의로 생성한 값에 의해 자동차가 전진하거나 정지하는 테스트 코드를 작성하는 도중 임의로 생성되는 값을 미리 테스트 코드내에서 지정해주고 이를 통해 기능을 테스트하고 싶었다.

    즉, 어떤 메소드를 실행했을 때 해당 메소드가 어떤 리턴 값을 리턴할 지를 정의해놓고 싶었다.

    이런 방법을 사용하기 위해서 Mockito를 이용했다.

     

    🔍 Mockito


    ❓ Mock

    💡 Mock
     - “모의”, “가짜의” 라는 뜻
     - 테스트할 때 필요한 실제 객체와 동일한 모의 객체를 만들어 테스트의 효율성을 높이기 위해 사용

     

    📖 위키에 정의된 Mock

    모의 객체(Mock Object)란 주로 객체 지향 프로그래밍으로 개발한 프로그램을 테스트할 경우 테스트를 수행할 모듈과 연결되는 외부의 다른 서비스나 모듈들을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는 데 사용하는 객체이다사용자 인터페이스(UI)나 데이터베이스 테스트 등과 같이 자동화된 테스트를 수행하기 어려운 때 널리 사용된다

     

    Mock 객체를 만들면 테스트 시간도 줄이고 불필요한 리소스 소비를 막고 객체의 행동까지 작성하는 사람 마음대로 조정할 수 있다.

     

    ❗Mockito

    Mockito는 개발자가 동작을 직접 제어할 수 있는 가짜 객체(Mock)를 지원하는 테스트 프레임워크이다.

    • Mockito를 활용하면 가짜 객체에 원하는 결과를 Stub하여 단위 테스트를 진행할 수 있다.

     

    🖥 사용방법

    🧩 Mock 객체 생성(의존성 주입) - 3가지 어노테이션 사용

    어노테이션 설명
    @Mock 가짜 객체를 만들어 반환해주는 어노테이션
    @Spy Stub하지 않은 메소드들은 원본 메소드를 그대로 사용하는 메소드
    @InjectMocks @Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 것.

     

    📍@Mock

     

    @Mock으로 만든 mock 객체는 가짜 객체이다!!!!

    • 따라서 해당 객체의 메소드를 호출하여 사용하기 위해서는 스터빙을 해주어야 한다.
    • 스터빙을 하지 않았다면 primitive type은 0, 참조형은 null을 반환하게 된다.

     

    public class TestMock {
    
        public int getNumber() {
            return 1;
        }
    
        public String getMessage() {
            return "Hello Mock!";
        }
    }

     

    @Mock 을 이용한 테스트 코드 작성

    class MockTest {
    
        @Mock
        TestMock testMock;
    
        @BeforeEach
        void setUp() {
            MockitoAnnotations.openMocks(this);
        }
    
        @Test
        void mockAnnotationTest() {
            assertThat(testMock.getNumber()).isZero();
            assertThat(testMock.getMessage()).isEqualTo(null);
    
        }
    }

     

    결과

     

     

    📍 @Spy

     

    @Spy로 만든 mock 객체는 진짜 객체이다.

    • 메소드 실행시 스터빙을 하지 않으면 기존 객체의 로직을 실행 후 반환
    • 스터빙을 했다면 스터빙한 값을 리턴

     

    public class TestSpy {
        public int getNumber() {
            return 2;
        }
    
        public String getMessage() {
            return "Hello Spy!";
        }
    }
    

     

    @Spy를 이용한 테스트 코드

    class MockTest {
    
        @Spy
        TestSpy testSpy;
    
        @BeforeEach
        void setUp() {
            MockitoAnnotations.openMocks(this);
        }
    
        @Test
        void spyAnnotationTest_doStubbing() {
            when(testSpy.getNumber()).thenReturn(3);
            when(testSpy.getMessage()).thenReturn("hi hi");
            assertThat(testSpy.getNumber()).isEqualTo(3);
            assertThat(testSpy.getMessage()).isEqualTo("hi hi");
    
        }
    
        @Test
        void spyAnnotationTest_doNotStubbing() {
            assertThat(testSpy.getNumber()).isEqualTo(2);
            assertThat(testSpy.getMessage()).isEqualTo("Hello Spy!");
    
        }
    }

     

    결과

     

    📍@InjectMocks

     

    @InjectMocks는 DI를 @Mock 이나 @Spy로 생성된 mock 객체를 자동으로 주입

     

    public class TestInjectMock {
        private TestMock testMock;
        private TestSpy testSpy;
    
        public int getTestMockNumber() {
            return testMock.getNumber();
        }
    
        public int getTestSpyNumber() {
            return testSpy.getNumber();
        }
    }
    

     

    @InjectMocks를 이용한 테스트 코드

    class MockTest {
    
        @InjectMocks
        TestInjectMock testInjectMock;
        @Mock
        TestMock testMock;
        @Spy
        TestSpy testSpy;
    
        @BeforeEach
        void setUp() {
            MockitoAnnotations.openMocks(this);
        }
    
        @Test
        void injectMockTest() {
            assertThat(testInjectMock.getTestMockNumber()).isZero();
            when(testSpy.getNumber()).thenReturn(3);
            assertThat(testInjectMock.getTestSpyNumber()).isEqualTo(3);
        }
    }

     

    결과

     

    ⭐ Stub


    📖 Test Stub

    • 테스트 호출 중 테스트 스텁은 테스트 중에 만들어진 호출에 대해 미리 준비된 답변을 제공하는 것

     

    ❗ 만들어진 mock 객체의 어떤 메소드를 실행했을 시 어떤 리턴 값을 리턴할지를 미리 정의

     

    💡 Mockito 에서의 Stub

     

    Mockito에서는 when 메소드를 이용해 스터빙을 지원

    - when에 스터빙을 적용할 메소드를 넣고 그 이후 어떤 동작을 어떤 식으로 처리할지 메소드 체이닝 방식을 이용해서 작성

     

     

    ✔️ 2가지 스터빙 방법

    1. ongoingStubbing
    2. Stubber

     

    ⛓️ OngoingStubbing

     

    when에 넣은 메소드의 리턴 값을 정의 해주는 메소드

    when({메소드}).{ongoingStubbing 메소드};
    

     

     

    메소드 설명
    thenReturn 스터빙한 메소드 호출 후 어떤 값을 리턴할지 정의
    thenThrow 스터빙한 메소드 호출 후 어떤 Exception을 throw 할지 정의
    thenAnswer 스터빙한 메소드 호출 후 어떤 작업을 할지 custome하게 정의
    - 굳이 사용하지 않고 thenReturn, thenThrow 를 사용하는 것을 추천하고 있다
    thenCallRealMethod 실제 메소드를 호출

     

    📍 Stubber

    Stubber는 OngoingStubbing과 달리 when에 스터빙할 클래스를 넣고 그 후 메소드를 호출한다.

    {Stubber 메소드}.when({스터빙할 클래스}).{스터빙할 메소드}
    

     

    메소드 설명
    doReturn 스터빙한 메소드 호출 후 어떤 값을 리턴할지 정의
    doThrow 스터빙한 메소드 호출 후 어떤 Exception을 throw 할지 정의
    doAnswer 스터빙한 메소드 호출 후 어떤 작업을 할지 custome하게 정의
    doNothing 스터빙한 메소드 호출 후 어떤 행동도 하지 않도록 정의
    doCallRealMethod 실제 메소드를 호출
    • 리턴 타입인 void 메소드 테스트가 가능하다..!

     

    📍예시

    List list = new LinkedList();
    List spy = spy(list);
    
    @Test
    void test() {
        // ongoingStubbing시 불가능하다.
        // list가 빈 객체이기 때문에 
        when(spy.get(0)).thenReturn("foo");
    
        // Stubber 방식을 이용
        doReturn("foo").when(spy).get(0);
    }

     

    🧑🏻‍💻 미션에 적용해보기

    OngoingStubbing 방식을 사용하는 것이 간결하고 코드를 이해하는데 더 좋다고 생각을 해

    사용했다.

     

    1️⃣ when().thenReturn()

    임의의 숫자를 생성하여 return하는 메소드의 return 값을 직접 지정하는 방식으로 테스트하는데 사용

    @ParameterizedTest
    @ValueSource(ints = {4, 5, 6, 7, 8, 9})
    void move_RandomValueMoreThanCriteria_IncreaseMoveCount(int value) {
        // given
        ...
        MockedStatic<Randoms> mockRandoms = mockStatic(Randoms.class);
    
        //when
        when(Randoms.pickNumberInRange(0, 9)).thenReturn(value);
        ...
    
        // then
        ...
        mockRandoms.close();
    }

     

    2️⃣ when().thenAnswer()

    경주게임을 끝내고 마지막 승리자를 제대로 결정하는지에 대한 테스트 코드를 작성하는 도중

    Randoms.pickNumberInRange()의 결과값으로 내가 파라미터로 넘겨주는 int형 배열의 값들을 이용하고 싶었다.

    • thenAnswer()를 통해 내가 원하는 값을 return하도록 custom 할 수 있겠다라고 생각했다.
    @ParameterizedTest
    @MethodSource("{}")
    void determineRaceWinners(String inputNameString, int[] randoms,
                                  int expectedWinnerCount, String[] expectedWinners) {
        // given
        ...
        MockedStatic<Randoms> mockRandoms = mockStatic(Randoms.class);
    
        // when
        when(Randoms.pickNumberInRange(0, 9)).thenAnswer(new Answer<Integer>() {
            private int callCount = 0;
            @Override
            public Integer answer(InvocationOnMock invocation) {
                return randoms[callCount++];
            }
        });
    
        ...
    
        mockRandoms.close();
     }

     

    ➕ MockedStatic

    우아한테크코스에서 제공해준 Randoms 클래스를 보면 클래스 내 메소드가 정적 메소드이다.

    public class Randoms {
        ...
        public static int pickNumberInRange(...) {
            ...        
        }
        ...
    }
    

    정적 메소드인 pickNumberInRange를 가짜로 사용해 return 값을 내가 원하는 값으로 지정하기 위해서 MockedStatic 기능을 사용했다.

     

    📖 참고자료

    Mock 객체 남용은 테스트 코드를 망친다.

    테스트 스텁(Test Stub)이란 무엇인가?

    효율적인 테스트를 위한 Stub 객체 활용법

    Mockito Tutorial | Baeldung

    Difference Between when() and doXxx() Methods in Mockito | Baeldung

    Mocking Static Methods With Mockito | Baeldung

     

    '☕️ java' 카테고리의 다른 글

    ArrayDeque  (0) 2024.03.15
    🔗 Java 문자열와 구분자  (1) 2023.10.30
    Multiple Assertions  (1) 2023.10.29
    ☕️ Java 17  (1) 2023.10.25
    🆚 AssertJ과 JUnit 의 Assertions 비교  (1) 2023.10.25

    댓글

Designed by Tistory.