[JUnit] Mock & Mockito -2

반응형

이번글에서는 Mock의 Fake 기능과, Stub를 대체하는 방법에 대해 알아보도록 하겠습니다.

1. Stub & Mock

이전까지의 글을 통해 우리는 Stub는 data를 test 할 때 사용하며, Mock은 behavior를 test 할 때 사용한다고 이해했습니다. 공통점으로는 Stub와 Mock 둘다 external dependency를 override 하는 방법으로 사용한다는 것 또한 이해할 수 있었습니다.

image.png

2. Fake

그렇다면 만약 test 할때 관심없는 부분들을 제외하고 test 하기 위해선 어떻게 해야될까요?

이와 같은 경우도 동일하게 Mock을 사용해 구현할 수 있습니다. 이를 우리는 Fake 라고 합니다.

image.png

예를 들어 test 하고자하는 business logic 내부에 아래와 같은 logging service가 존재한다고 가정해보겠습니다.

image.png

만약 이때 Fake를 사용하지 않고 test를 수행한다면, logging service는 log를 찍어 낼 것입니다. 하지만, 우리는 business logic을 test 하고 싶은 것이지 log가 제대로 찍히는지 안찍히는지는 전혀 관심이 없습니다. 즉 test 대상에서 아예 제외하고 싶습니다.

이때 우리는 Logging service를 Mock 인스턴스로 만들어 Logging service의 모든 메서드가 수행되지 않도록 만들 수 있습니다.

image.png

이를 통해 우리는 test에 본질에 집중할 수 있으며, 자원 낭비 또한 방지할 수 있습니다.

3. Replace Stub

이번에는 Mock을 사용해 Stub를 대체해 보도록 하겠습니다.

물론 Stub을 그냥 사용해도 되지만, Mock을 사용하면 조금더 깔끔하고 가독성 좋게 test code를 작성할 수 있습니다.

image.png

동일하게 ISBN number 예제를 사용하도록 하겠습니다. 우리는 이전까지 아래와 같은 test code를 작성했습니다.

StockManagementTest (stub)

 @Test
    public void canGetCorrectLocatorCode(){

        // Stub
        ExternalISBNDataService testWebService = new ExternalISBNDataService() {
            @Override
            public Book lookup(String isbn) {
                return new Book(isbn, "Of Mice And Men", "J. Streinbeck");
            }
        };

        // Stub
        ExternalISBNDataService testDatabaseService = new ExternalISBNDataService() {
            @Override
            public Book lookup(String isbn) {
                return null;
            }
        };

        // set service
        StockManager stockManager = new StockManager();
        stockManager.setDatabaseService(testDatabaseService);
        stockManager.setWebService(testWebService);

        // do
        String isbn = "014077396";
        String locatorCode = stockManager.getLocatorCode(isbn);

        // assert
        assertEquals("7396J4", locatorCode);
    }

위의 test code는 external service의 dependency를 무시하기 위해 Stub를 사용해 lookup 메서드를 override 했습니다. 이때 Mock을 사용해 Stub를 대체하면 아래와 같이 test code를 변경할 수 있습니다.

StockManagementTest (mock)

@Test
    public void canGetCorrectLocatorCode(){

        // database service mock
        ExternalISBNDataService testDatabaseService = mock(ExternalISBNDataService.class);
        when(testDatabaseService.lookup(anyString())).thenReturn(null);

        // web service mock
        ExternalISBNDataService testWebService = mock(ExternalISBNDataService.class);
        when(testWebService.lookup(anyString())).thenReturn(new Book("014077396", "Of Mice And Men", "J. Streinbeck"));

        // set service
        StockManager stockManager = new StockManager();
        stockManager.setDatabaseService(testDatabaseService);
        stockManager.setWebService(testWebService);

        // do
        String isbn = "014077396";
        String locatorCode = stockManager.getLocatorCode(isbn);

        // assert
        assertEquals("7396J4", locatorCode);
    }

테스트 해보겠습니다.

image.png

여전히 테스트를 잘 통과하는 것을 확인할 수 있습니다. 위와 같이 Mock을 사용해 Stub을 대체할 수 있습니다.

4. Setup

이번에는 위에서 작성한 test code를 refactoring 해보도록 하겠습니다. refactoring 할 전체 test code는 아래와 같습니다.

StockManagementTest (before refactoring)

public class StockManagementTest {

    @Test
    public void canGetCorrectLocatorCode(){

        // database service mock
        ExternalISBNDataService testDatabaseService = mock(ExternalISBNDataService.class);
        when(testDatabaseService.lookup(anyString())).thenReturn(null);

        // web service mock
        ExternalISBNDataService testWebService = mock(ExternalISBNDataService.class);
        when(testWebService.lookup(anyString())).thenReturn(new Book("014077396", "Of Mice And Men", "J. Streinbeck"));

        // set service
        StockManager stockManager = new StockManager();
        stockManager.setDatabaseService(testDatabaseService);
        stockManager.setWebService(testWebService);

        // do
        String isbn = "014077396";
        String locatorCode = stockManager.getLocatorCode(isbn);

        // assert
        assertEquals("7396J4", locatorCode);
    }

    @Test
    public void databaseIsUsedIfDataIsPresent(){

        // service mock 
        ExternalISBNDataService databaseService = mock(ExternalISBNDataService.class);
        ExternalISBNDataService webService = mock(ExternalISBNDataService.class);

        // set return value
        when(databaseService.lookup("014077396")).thenReturn(new Book("014077396", "abc", "abc"));

        // set service
        StockManager stockManager = new StockManager();
        stockManager.setDatabaseService(databaseService);
        stockManager.setWebService(webService);

        // do
        String isbn = "014077396";
        String locatorCode = stockManager.getLocatorCode(isbn);

        // verify
        verify(databaseService, times(1)).lookup("014077396");
        verify(webService, times(0)).lookup(anyString());
    }

    @Test
    public void webserviceIsUsedIfDataIsNotPresentInDatabase(){

        // service mock 
        ExternalISBNDataService databaseService = mock(ExternalISBNDataService.class);
        ExternalISBNDataService webService = mock(ExternalISBNDataService.class);

        // set return value
        when(databaseService.lookup("014077396")).thenReturn(null);
        when(webService.lookup("014077396")).thenReturn(new Book("014077396", "abc", "abc"));

        // set service
        StockManager stockManager = new StockManager();
        stockManager.setDatabaseService(databaseService);
        stockManager.setWebService(webService);

        // do
        String isbn = "014077396";
        String locatorCode = stockManager.getLocatorCode(isbn);

        // verify
        verify(databaseService, times(1)).lookup("014077396");
        verify(webService, times(1)).lookup("014077396");

    }

}

위의 test code에서는 총 3개의 테스트를 수행하고 있습니다. 하지만 code를 자세히 살펴보면 많은 부분에서 code가 겹치고 있는 것을 확인할 수 있습니다. 따라서 중복된 코드를 통합해 refactoring 할 필요가 있습니다.

이떄 사용할 수 있는 방법이 @Before 입니다. @Before 어노테이션에 선언한 로직은 각 test 수행전 매번 실행됩니다. @Before를 사용해 refactoring 해보겠습니다.

✔ Junit5에서는 @Before -> @BeforeEach 로 변경되었습니다 😊.

StockManagementTest (after refactoring)

public class StockManagementTest {

    // field
    private ExternalISBNDataService testDatabaseService;
    private ExternalISBNDataService testWebService;
    private StockManager stockManager;

    @BeforeEach
    public void setup(){
        // mock
        testDatabaseService = mock(ExternalISBNDataService.class);
        testWebService = mock(ExternalISBNDataService.class);

        //set service
        stockManager = new StockManager();
        stockManager.setDatabaseService(testDatabaseService);
        stockManager.setWebService(testWebService);
    }

    @Test
    public void canGetCorrectLocatorCode(){

        // set return value
        when(testDatabaseService.lookup(anyString())).thenReturn(null);
        when(testWebService.lookup(anyString())).thenReturn(new Book("014077396", "Of Mice And Men", "J. Streinbeck"));

        // do
        String isbn = "014077396";
        String locatorCode = stockManager.getLocatorCode(isbn);

        // assert
        assertEquals("7396J4", locatorCode);
    }

    @Test
    public void databaseIsUsedIfDataIsPresent(){

        // set return value
        when(testDatabaseService.lookup("014077396")).thenReturn(new Book("014077396", "abc", "abc"));

        // do
        String isbn = "014077396";
        String locatorCode = stockManager.getLocatorCode(isbn);

        // verify
        verify(testDatabaseService, times(1)).lookup("014077396");
        verify(testWebService, times(0)).lookup(anyString());
    }

    @Test
    public void webserviceIsUsedIfDataIsNotPresentInDatabase(){

        // set return value
        when(testDatabaseService.lookup("014077396")).thenReturn(null);
        when(testWebService.lookup("014077396")).thenReturn(new Book("014077396", "abc", "abc"));

        // do
        String isbn = "014077396";
        String locatorCode = stockManager.getLocatorCode(isbn);

        // verify
        verify(testDatabaseService, times(1)).lookup("014077396");
        verify(testWebService, times(1)).lookup("014077396");
    }
}

테스트 해보겠습니다.

image.png

위와 같이 3개의 test 모두 정상적으로 통과하는 것을 확인할 수 있습니다.

한눈에 보기에도 test code가 한결 깔끔해진 것을 확인 할 수 있습니다 👏👏👏


    참고 자료 : [https://www.udemy.com/course/practical-test-driven-development-for-java-programmers/](https://www.udemy.com/course/practical-test-driven-development-for-java-programmers/)

추천서적

 

자바와 JUnit을 활용한 실용주의 단위 테스트

COUPANG

www.coupang.com

파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음


반응형

'JUnit' 카테고리의 다른 글

[JUnit] Spy  (0) 2020.08.20
[JUnit] Bad Test  (0) 2020.08.20
[JUnit] Mock & Mockito -1  (0) 2020.08.20
[JUnit] Stub  (0) 2020.08.20
[JUnit] TDD -2  (0) 2020.08.17

댓글

Designed by JB FACTORY