Spring Boot & 테스트 코드 작성 (2) - Mock, Mockito

Spring Boot & 테스트 코드 작성 (2) - Mock, Mockito

이전글

앞서 JUnit 을 이용한 JAVA 테스트 코드 실행을 해보았다.
이번엔 Spring Boot 를 이용할 경우 사용하게 되는 Controller 테스트 방법을 배워본다.
Controller 의 경우 어노테이션을 통해 자동생성되는 기능과 개별 API 등을 테스트 해야하므로 다양한 기법들이 사용된다.
우선 Controller 를 테스트하기 위한 Mock 개념에 대해 배워보자

Mock

Mock 이란 테스트 더블이라고도 하며, 실제 사용되어야 하는 객체의 대역(Alternate) 를 의미한다.
Mock 객체는 대상의 행위를 검증하는데 있어서 사용하기 때문에 객체가 가지고 있어야 하는 기본적인 정보를 반드시 갖고 있다. 즉, 자동차라는 객체의 행동인 '운전' 을 검증한다고 하면 엑셀을 '밟는' 행위를 테스트 해야하며 이를 위해 '자동차' 라는 Mock 객체는 주유 상태 (가득, 위험, 없음), 엔진 상태 (시동, 정지) 등을 미리 가지고 있어야 한다.

Stub 과 Mock Object

마틴 파울러는 자신의 글에서 Mock Object 는 행위 검증에 사용하고, Stub은 상태를 검증하는데 사용하는 것이라고 말했다.

There is a difference in that the stub uses state verification while the mock uses behavior verification.
원문 : http://martinfowler.com/articles/mocksArentStubs.html

이러한 Mock 객체를 직접 생성하는 경우도 있지만 일일이 클래스를 만들기는 어려울 것이다. 그래서 Mockito 라는 라이브러리를 이용하도록 하겠다.

Mockito

Mockito는 JUnit위에서 동작하며 Mocking과 Verification을 도와주는 프레임워크이다.

Mockito Test 예제

Mockito 추가

build.gradle의 dependencies에 mockito 라이브러리를 추가해준다.

testImplementation 'org.mockito:mockito-all:1.9.5'

빌드를 하고나면 mockito 라이브러리가 추가가 되고
다음과 같은 테스트 코드를 작성한다.

package com.example3.demo;

...
import com.example3.demo.entity.Product;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class ProductUnitTest {
    @Mock 
    Product product;

    @Test
    public void mockTest() {
        MockitoAnnotations.initMocks(this);
        // OR Product product = mock(Product.class); 
        // 위와 같은 내용으로 IoC 가 적용되어 mockito 에서 객체를 생성한다.

        assertTrue(product != null);

        System.out.println(product.getProdID()+""); // 0
        System.out.println(product.getProdName()+""); // null
    }
}

Product 라는 객체를 임의로 생성하였고, Mockito 라이브러리를 통해 초기화를 해주었다.
때문에 assertTruepass 가 되며 product 객체안의 값들은 기본값들로 초기화 된다. 다만 객체 타입의 경우 null로 초기화 되어있다.

이렇게 초기화 되어있는 객체는 바로 사용할 수가 없다. 왜냐하면 내부에 지정된 값이 없어서 테스트가 정상적으로 진행되지 않기 때문이다.

조건 설정

이럴때 when() 을 사용하여 특정 메소드 호출시 특정 값을 반환하도록 지정한다.

    @Test
    public void mockDataTest() {
        MockitoAnnotations.initMocks(this);
        // OR Product product = mock(Product.class);
        // 위와 같은 내용으로 IoC 가 적용되어 mockito 에서 객체를 생성한다.

        assertTrue(product != null);

        when(product.getProdId()).thenReturn(3);
        when(product.getProdName()).thenReturn("Black M2M T-Shirt");
        when(product.getProdPrice()).thenReturn(13000);
        when(product.getProdSaleRate()).thenReturn(20);

        System.out.println(product.getProdId()+"");
        System.out.println(product.getProdName()+"");
        System.out.println(product.getProdPrice()+""); // 13000
        System.out.println(product.getProdSaleRate()+""); // 20
        System.out.println(product.getProdSalePrice()+""); // 0

        assertEquals(product.getProdId(), 3, "Product ID");
        assertEquals(product.getProdName(), "Black M2M T-Shirt", "Product Name");
        assertEquals(product.getProdPrice(), 13000, "Origin Price");
        assertEquals(product.getProdSalePrice(), 10400, "Sale Price");
    }

2_mockito_test 실패

이렇게 하면 마지막 Sale Price 에서 에러가 발생하게 된다.
왜냐하면 getProdSalePrice()는 가격과 할인율을 계산하는데 Mockito 라이브러리는 Product 객체의 각 메소드 리턴값을 바꾸는 것이므로 멤버변수들의 값이 바뀌진 않기 때문이다. 이러한 상황을 해결하려면 실제 객체를 생성해 주어야 한다.

우선 본 예제에서는 getProdSalePrice() 을 주석처리 해주도록 하겠다.

2_mockito_test 결과

예외 설정

doThrow

doThrow 를 이용하여 특정 값이 들어갔을 경우 예외를 발생 시킬 수 있다.
다만 doThrow를 사용하려면 예외가 발생되는 함수가 void method 이여야 한다.

Use doThrow() when you want to stub the void method with an exception.
원문 : https://javadoc.io/static/org.mockito/mockito-core/3.3.3/org/mockito/Mockito.html#doThrow-java.lang.Throwable...-


    @Test
    public void mockDoThrowTest() {
        MockitoAnnotations.initMocks(this);

        assertTrue(product != null);

        // doThrow
        doThrow(new Exception("Do Throw Exception")).when(product).setProdPrice(eq(10000));
        product.setProdPrice(100);
        product.setProdPrice(1000);
        product.setProdPrice(10000); // Exception!!
        product.setProdPrice(100000);
    }

실행 결과

2_mokito_doThrow

thenThrow

thenThrow 는 앞서 배운 when() 구문에서 사용할 수 있는데 특정 메소드 실행시 메소드가 무조건 예외를 발생시키거나 혹은 상황에 따라 다르게 동작 할 수 있도록 설정 할 수 있다.

you can set different behavior for consecutive method calls.
Last stubbing (e.g: thenReturn("foo")) determines the behavior of further consecutive calls.
원문 : https://javadoc.io/static/org.mockito/mockito-core/3.3.3/org/mockito/Mockito.html#method.detail

우선 무조건 에러가 발생되는 경우를 확인해보면
다음과 같은 상황에서 getProdPrice를 호출 하기만 해도 에러가 발생한다.

    @Test
    public void mockThenThrowTest() {
        MockitoAnnotations.initMocks(this);

        assertTrue(product != null);

        when(product.getProdPrice())
            .thenThrow(new RuntimeException("Then Throw Exception"))
            .thenReturn(10000);

        product.getProdPrice(); // 이미 에러 발생

        assertEquals(product.getProdPrice(), 10000, "Origin Price");
    }

실행 결과

2_mockito_thenThrow1

이런 경우는 다음과 같이 실행하여 여러번 실행시 동작을 변경할 수 있다.

    @Test
    public void mockThenThrowTest() {
        MockitoAnnotations.initMocks(this);

        assertTrue(product != null);

        when(product.getProdPrice())
            .thenThrow(new RuntimeException("Then Throw Exception"))
            .thenReturn(10000);

        try {
            product.getProdPrice(); // Error!
        } catch(Exception e) {}

        assertEquals(product.getProdPrice(), 10000, "Origin Price"); // 정상 동작
    }

실행 결과

2_mockito_thenThrow2

또 이를 응용하여 List 형태에서도 조건에 따라 사용할 수 있다.


    @Test
    public void mockListThenThrowTest() {
        MockitoAnnotations.initMocks(this);

        assertTrue(product != null);
        when(product.getProdName()).thenReturn("Normal Obj");

        ArrayList<Product> prodList = mock(ArrayList.class);

        when(prodList.get(0))
            .thenReturn(product);
        when(prodList.get(1))
            .thenThrow(new RuntimeException("Then Throw Exception"));

        System.out.println(prodList.get(0).getProdName()+"");
        System.out.println(prodList.get(1).getProdName()+""); // Error!
    }

실행 결과

2_mockito_thenThrow3

엮인글

Spring Boot & 테스트 코드 작성 (1) - JUnit
Spring Boot & 테스트 코드 작성 (2) - Mock, Mockito
Spring Boot & 테스트 코드 작성 (3) - MockMvc
Spring Boot & Rest API (1)
Spring Boot & Rest API (2) - JPA, MySQL


참조

Mock Object란 무엇인가?
마틴파울러 - Mocks Aren't Stubs
[Mockito] Mockito를 이용한 Unit Test

+ Recent posts