[JUnit] Mock & Mockito -2
- JUnit
- 2020. 8. 20.
이번글에서는 Mock의 Fake 기능과, Stub를 대체하는 방법에 대해 알아보도록 하겠습니다.
1. Stub & Mock
이전까지의 글을 통해 우리는 Stub는 data를 test 할 때 사용하며, Mock은 behavior를 test 할 때 사용한다고 이해했습니다. 공통점으로는 Stub와 Mock 둘다 external dependency를 override 하는 방법으로 사용한다는 것 또한 이해할 수 있었습니다.
2. Fake
그렇다면 만약 test 할때 관심없는 부분들을 제외하고 test 하기 위해선 어떻게 해야될까요?
이와 같은 경우도 동일하게 Mock을 사용해 구현할 수 있습니다. 이를 우리는 Fake 라고 합니다.
예를 들어 test 하고자하는 business logic 내부에 아래와 같은 logging service가 존재한다고 가정해보겠습니다.
만약 이때 Fake를 사용하지 않고 test를 수행한다면, logging service는 log를 찍어 낼 것입니다. 하지만, 우리는 business logic을 test 하고 싶은 것이지 log가 제대로 찍히는지 안찍히는지는 전혀 관심이 없습니다. 즉 test 대상에서 아예 제외하고 싶습니다.
이때 우리는 Logging service를 Mock 인스턴스로 만들어 Logging service의 모든 메서드가 수행되지 않도록 만들 수 있습니다.
이를 통해 우리는 test에 본질에 집중할 수 있으며, 자원 낭비 또한 방지할 수 있습니다.
3. Replace Stub
이번에는 Mock을 사용해 Stub를 대체해 보도록 하겠습니다.
물론 Stub을 그냥 사용해도 되지만, Mock을 사용하면 조금더 깔끔하고 가독성 좋게 test code를 작성할 수 있습니다.
동일하게 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);
}
테스트 해보겠습니다.
여전히 테스트를 잘 통과하는 것을 확인할 수 있습니다. 위와 같이 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");
}
}
테스트 해보겠습니다.
위와 같이 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' 카테고리의 다른 글
[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 |