[JUnit] Mock & Mockito -1

반응형

이번글에서는 JUnit의 Mock을 활용해 behavior를 테스트 해보도록 하겠습니다.

1. Test Behavior

1-1) Test Result

이전 글과 동일하게 ISBN Number를 예를 들어보겠습니다.

우리는 이전 글에서 Assert를 사용해 최종 결과값과 기대하는 값을 비교하는 테스트를 진행했었습니다. 예를 들어 getLocatorCode("014077396")의 최종 결과값이 "7396J4"와 같은지를 테스트 했습니다. 즉, 우리는 result를 test 했었다고 이해할 수 있습니다.

Test Result Example

@Test
public void canGetCorrectLocatorCode(){

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

    // set service
    StockManager stockManager = new StockManager();
    stockManager.setService(service);

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

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

1-2) Test Behavior

그렇다면 만약에 business logic의 result가 아닌 behavior를 test 하고자 한다면 어떻게 해야될까요?.. 😅

예를 들어 local Database와 Web Service라는 2개의 external service가 있다고 가정해보겠습니다. 이때 각각의 서비스는 동일하게 Book 정보를 넘겨주는 lookup 메서드를 소유하고 있습니다.

이때 우리는 database에 book 정보가 있다면 database에서 정보를 가져오고, database에 book 정보가 없다면 web service에서 book 정보를 가져오도록 설계하고자 합니다.

image.png

위와 같은 상황에서 우리가 의도한 대로 business logic이 행동하는지를 test 하고자 할때, 이를 behavior를 test 한다고 이야기합니다.

따라서 우리는 test code를 통해 database에 book 정보가 있을 때에는 webservice를 호출하지 않고, database에 book 정보가 없을 때에만 webservice를 호출하는지를 test 하고자 합니다.

이때 우리는 '제대로된 결과값을 해당 메서드가 return' 하는가? 에 대해서는 전혀 관심이 없습니다. 우리가 test 하고자 하는 것은 특정상황에서 '의도한 대로 business logic이 행동'하는가? 입니다.

이해가 안되시죠?..😅 코드로 해당 내용을 다시한번 살펴보겠습니다.

2. Fail

먼저 실패하는 Test를 생성해봅시다.

StockManagementTest에 아래와 같이 2개의 test를 추가합니다.

첫번째 test는 database에 book 정보가 있을경우 database를 실제로 사용하는지 테스트합니다. 두번째 test는 database에 book 정보가 없을 경우 webservice를 사용하는지 테스트합니다.

다시한번 강조하지만, 우리는 해당 business logic의 결과값을 확인하고 싶은것이 아니라 특정 조건에서 특정 로직을 제대로 수행하는지에 대한 '행동'을 test 하는 것이 목적입니다.

StockManagementTest

@Test
public void databaseIsUsedIfDataIsPresent(){
    fail("Not yet implemented");
}

@Test
public void webserviceIsUsedIfDataIsNotPresentInDatabase(){
    fail("Not Yet Implemented");
}

테스트 해보겠습니다.

image.png

두개의 테스트 모두 실패했습니다. 다음 단계로 넘어가겠습니다. 😁

3. Pass

이제 우리는 pass하기 위한 아주 간단한 code를 작성해야 합니다.

그러나, 이전까지 우리가 살펴본 방식으로는 이를 테스트 할 수 없습니다. Assert는 최종 결과값만을 test할 수 있습니다.

따라서 우리는 행동을 테스트 할 수 있는 Mock 객체를 사용해야 합니다. Mockito 라이브러리의 Mock 객체는 'wheher or not a method was called'를 test 할 수 있습니다.

image.png

이제 기본적인 개념은 알았으니 Mock을 사용해 test code를 작성해보겠습니다.

4. Mock

4-1) Change StockManager

먼저 StockManager 클래스가 database service와 web service를 사용할 수 있도록 아래와 같이 code를 수정합니다.

StockManager

public class StockManager {

    // database service
    private ExternalISBNDataService databaseService;
    // web service
    private ExternalISBNDataService webService;

    // set database service
    public void setDatabaseService(ExternalISBNDataService databaseService){
        this.databaseService = databaseService;
    }

    // set seb service
    public void setWebService(ExternalISBNDataService webService){
        this.webService = webService;
    }

    public String getLocatorCode(String isbn) {
       // check database have book info
       Book book = databaseService.lookup(isbn);

       // database have no data -> then call web service
       if(book == null){
           book = webService.lookup(isbn);
       }

       // business logic
       StringBuilder locator = new StringBuilder();
       locator.append(isbn.substring(isbn.length() - 4));
       locator.append(book.getAuthor().substring(0,1));
       locator.append(book.getTitle().split(" ").length);

       // return 
       return locator.toString();
    }
}

getLocatorCode 메서드에서는 databaseService의 lookup이 null을 반환할 경우에만 webService의 lookup을 호출합니다.

4-2) Create Mock Instance

다음으로 test code는 아래와 같이 작성합니다.

StockManagementTest

@Test
public void databaseIsUsedIfDataIsPresent(){
    // create mock instance
    ExternalISBNDataService databaseService = mock(ExternalISBNDataService.class);
    ExternalISBNDataService webService = mock(ExternalISBNDataService.class);

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

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

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

위의 코드에서는 databaseService와 webService를 mock 객체로 생성했습니다. mock으로 생성한 객체는 dummy 객체로 실제 내부에 구현된 메서드는 수행하지 않고, 특정상황에서 특정 메서드나 business logic이 수행되지는만 체크합니다.

한번 테스트해볼까요? 🧑

image.png

위와 같이 NullPointerException이 발생했습니다. 이유는 위에서 mock 객체를 사용했기 때문입니다. 말씀드린바와 같이 mock으로 생성한 dummy 객체는 내부 메서드를 실제로 수행하지 않습니다.

즉, StockManager의 getLocationCode 내부의 lookup(isbn) 메서드는 실제로 수행되지 않고 Null을 반환합니다.

image.png

4-3) When -> ThenReturn

따라서 Mock 객체를 제대로 사용하기 위해서는 아래와 같이 특정 메서드의 return 값을 명시해줘야 합니다. 이는 이전글에서 보았던 Stub와 동일한 개념입니다. 이를 통해 Mock으로 생성한 객체의 메서드는 하드코딩한 결과값을 return 하게 됩니다.

StockManagementTest

@Test
public void databaseIsUsedIfDataIsPresent(){
    // create mock instance
    ExternalISBNDataService databaseService = mock(ExternalISBNDataService.class);
    ExternalISBNDataService webService = mock(ExternalISBNDataService.class);

    // set hard coding 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);

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

Stub와 다른점은 아래와 같이 input parameter 값도 명시해줘야 한다는 점입니다.

 when(databaseService.lookup("014077396")).thenReturn(new Book("014077396", "abc", "abc"));

4-4) Remove Assert

이제 databaseService Mock 객체는 lookup 메서드에 input 값으로 "014077396"이 들어올 경우 하드코딩한 결과값인 new Book("014077396", "abc", "abc")을 return 하게 됩니다.

다시한번 테스트해볼까요? 🧑

image.png

여전히 테스트는 실패하고있습니다. 하지만, 뭔가 달라졌습니다. 이전의 NullPointerException이 아닌 AssertFailed 가 발생했습니다. 뭔가 이상하지 않으신가요 🤔🤔🤔??.

맞습니다, 이번 test의 목적은 '행동'을 테스트하는 것 이었습니다. 따라서 Assert를 통한 결과값 test는 불필요합니다. 따라서 이를 제거합니다.

StockManagementTest

@Test
public void databaseIsUsedIfDataIsPresent(){
    // create mock instance
    ExternalISBNDataService databaseService = mock(ExternalISBNDataService.class);
    ExternalISBNDataService webService = mock(ExternalISBNDataService.class);

    // set hard coding 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);

}

4-5) Verify

이제 마지막입니다. 🙂

앞서 설명드렸다 싶이 test code를 통해 database에 book 정보가 있을 때에는 webservice를 호출하지 않고, database에 book 정보가 없을 때에만 webservice를 호출하는지를 확인해야 합니다.

image.png

이와 같은 행동을 검사하기 위해 Verify를 사용할 수 있습니다. Verify는 명시한 메서드를 특정 parameter가 n time 호출했는지를 확인합니다.

성적을 확인하는게 아니라.. 공부를 했는지 안했는지만 확인하다고 생각하면 될 것 같습니다 😅.

Verify 메서드를 아래와 같이 추가해봅시다.

@Test
public void databaseIsUsedIfDataIsPresent(){
     // create mock instance
    ExternalISBNDataService databaseService = mock(ExternalISBNDataService.class);
    ExternalISBNDataService webService = mock(ExternalISBNDataService.class);

    // set hard coding 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());
}

이제 Verify는 databaseService의 lookup 메서드가 input 값으로 "014077396"를 "1번" 호출했는지를 검사합니다. 또한 webservice의 lookup 메서드는 input 값으로 아무 String 값으로 "0"번 호출했는지를 검사합니다. 즉, webservice의 lookup 메서드는 단 한번도 호출되지 않았는지를 검사합니다.

테스트 해보겠습니다.

image.png

드디어 테스트가 통과했습니다. 이로써 우리는 각 서비스가 우리가 원하는 business logic을 기반으로 '행동'하고 있다는 것을 확인할 수 있게 되었습니다 👏👏👏.

database에 data가 없을 경우 webservice를 호출하는 테스트는 아래와 같이 작성할 수 있습니다.

StockManagementTest

@Test
public void webserviceIsUsedIfDataIsNotPresentInDatabase(){
    // create mock instance
    ExternalISBNDataService databaseService = mock(ExternalISBNDataService.class);
    ExternalISBNDataService webService = mock(ExternalISBNDataService.class);

    // set hard coding 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");

}

databseService

코드를 해석해보면 databaseService Mock 인스턴스는 lookup("014077396")이 호출되면, 반환값으로 null을 return 합니다. 따라서 이때 1번 lookup("014077396")를 호출하므로 verify를 통과합니다.

// mock
when(databaseService.lookup("014077396")).thenReturn(null);

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

webService

databaseService 에서 book 정보를 null로 넘겨주었으므로 webService의 lookup("014077396")을 호출합니다. 이때 반환값은 new Book("014077396", "abc", "abc") 이지만, 우리의 test에서는 신경쓰지 않습니다. 대신 이때 동일하게 1번 lookup("014077396")이 호출되었으므로 verfiy를 통과합니다.

// mock
when(webService.lookup("014077396")).thenReturn(new Book("014077396", "abc", "abc"));

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

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


추천서적

 

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

COUPANG

www.coupang.com

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


반응형

'JUnit' 카테고리의 다른 글

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

댓글

Designed by JB FACTORY