티스토리 뷰
이전 글: https://gojs.tistory.com/72
테스트 코드의 장점 활용하기
테스트 코드를 작성하는 것은 단순하게 구현된 기능을 테스트한다는 의미 이상의 효과를 발휘한다.여러 긍정적인 효과를 발휘하고 테스트 자체로도 품질 지표가 되기도 한다.그렇다면 테스트
gojs.tistory.com
단위 테스트는 코드 내의 단위(클래스, 메서드) 범위를 테스트하기 때문에 다른 객체나 외부 리소스에 의존성을 가지는 경우 테스트를 작성하기 까다롭다.
예를 들면 데이터베이스에 의존하는 경우 데이터베이스에 데이터가 준비되어있어야 한다던지, 테스트하고자하는 영역 이외에 신경써야할 부분이 많아진다.
또한 그렇게 테스트를 진행한다고 하더라도 데이터베이스 연결까지 포함하여 테스트하기 때문에 단위 테스트보다는 통합 테스트나 기능 테스트에 더욱 가깝다.
이러한 경우에 활용할 수 있는 두 가지 방법이 있는데 그것이 스텁과 모의 객체이다.
스텁 활용하기
스텁이란 테스트를 위한 테스트용 객체이다.
간단히 예를 들면 HTTP 통신을 위한 부분이 단위 테스트에 포함된다고 가정한다면, HTTP 통신을 담당하는 테스트 클래스를 따로 생성해서 테스트에 활용할 수 있다.
public class StockService {
private StockClient stockClient;
public StockService(StockClient stockClient) {
this.stockClient = stockClient;
}
public double retrieveTotalStockPrice(List<Long> stockIds) {
return stockIds.stream()
.map(id -> stockClient.retrievePrice(id))
.mapToDouble(price -> price)
.sum();
}
}
public interface StockClient {
double retrievePrice(Long stockId);
}
위 코드에서 StockService의 retrieveTotalStockPrice(...) 메서드를 호출하면 전달한 모든 stockId에 대해 각각 API 통신을 일으켜 가격을 받아오고 합산한다.
그렇지만 StockClient가 아직 구현되지 않은 상태라고 가정하면 당장 가격 정보를 받아올 수 없으니 테스트가 어려울 것이다.
또한 외부 리소스를 함께 연동하여 테스트하기 때문에 단위 테스트는 아니다.
그렇다면 이 상황에서 StockClient를 임시로 구현하는 스텁 클래스를 만들고, 이를 활용해 StockService를 테스트해보자.
public class StockServiceTest {
private final Map<Long, Double> STOCK_PRICES = Map.of(
1L, 1000D,
2L, 3000D,
4L, 5000D,
7L, 100D);
@Test
void testRetrieveTotalStockPrice() {
StockService stockService = new StockService(new StubStockClient(STOCK_PRICES));
List<Long> stockIds = List.of(1L, 4L);
double totalPrice = stockService.retrieveTotalStockPrice(stockIds);
Assertions.assertEquals(6000D, totalPrice);
}
private class StubStockClient implements StockClient {
private Map<Long, Double> stockPrices;
public StubStockClient(Map<Long, Double> stockPrices) {
this.stockPrices = stockPrices;
}
@Override
public double retrievePrice(Long stockId) {
return stockPrices.get(stockId);
}
}
}
위와 같이 테스트 코드 아래에 StubStockClient 클래스를 구현했다.
이 클래스는 실제 통신을 일으키지 않고 생성자에 주어진 가격 정보를 가지고 적절한 값의 반환만 수행한다.
따라서 테스트하는 시점에서 주식 가격 정보를 스텁 객체에 주입하고 이 StubStockClient 객체를 다시 StockService 객체에 주입한다.
이렇게 StockService의 retrieveTotalStockPrice(...) 메서드의 외부 의존성과 상관없이 단위 테스트를 진행할 수 있다.
결과적으로 StockClient가 구현되어있지 않은 상태임에도 StockService에 대한 테스트를 수행할 수 있었고 위와 같이 JaCoCo Report를 생성했다.
모의객체 활용하기
스텁은 굉장히 유용한 방법이기도 하지만, 스텁을 활용하기 위해서는 매번 특정 결과를 내는 테스트용 클래스를 생성해야한다.
또한 단위 테스트의 검증 범위에는 단순히 특정 입력에 대한 결과값의 검증 뿐아니라 실행되어야하는 로직이 실행되었는지도 포함되어야하나, 스텁은 이런 부분까지는 검증이 어렵다.
모의 객체를 활용하게된다면 단위 테스트 내에서 테스트 내의 특정 동작에 대해 제어가 가능하다.
Mockito 활용해보기
EasyMock이나 JMock에 비해 월등히 많이 사용되는 Mock Framework이기 때문에 Mockito를 활용하여 모의 객체를 알아볼 것이다.
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
// https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
Mockito를 사용하려면 위와 같이 의존성을 추가해야한다.
@ExtendWith(MockitoExtension.class)
public class TestStockServiceMockito {
@Mock
private StockClient mockStockClient;
@Test
public void testRetrieveTotalStockPriceOK() {
List<Long> stockIds = List.of(1L, 4L);
when(mockStockRepository.findById(1L)).thenReturn(1000D);
when(mockStockRepository.findById(4L)).thenReturn(5000D);
StockService stockService = new StockService(mockStockClient);
double totalPrice = stockService.retrieveTotalStockPrice(stockIds);
assertEquals(6000D, totalPrice);
verify(mockStockClient, times(2)).retrievePrice();
}
}
기존에 스텁을 구현하여 작성했던 예시 코드를 위와 같이 Mockito를 활용하여 수정해보았다.
@Mock 어노테이션을 명시하여 모의 객체를 설정한다. 이 모의 객체는 스텁의 역할을 함께 수행한다.
Mockito.when(...).thenReture(...)을 활용하여 모의 객체에 입력값에 대한 기대 출력값을 설정한다.
그리고 모의 객체를 테스트할 객체에 의존성 주입하고 기존과 동일하게 테스트를 진행한다.
위 코드에서 모의 객체가 스텁과 가장 차별있게 구현된 부분은 Mockito.verify(...) 메서드를 사용하여 결과 값뿐 아니라 실행되어야하는 동작이 정확하게 수행되었는지도 검증되는 부분이다.
위와 같이 Mockito를 사용하게 되면 기존에 개발자가 직접 테스트용 클래스를 작성하던 것과 달리, 굉장히 쉽고 편리하게 외부 리소스와 격리하여 단위 테스트를 작성할 수 있다.
스텁과 모의 객체의 차이점과 Mockito에서 혼란스러운 부분
여러가지 자료를 찾아보면 스텁과 모의 객체의 가장 큰 차이점은 무엇을 검증하는 것인가이다.
스텁은 특정한 입력값에 대해서 특정한 출력값이 정상적으로 출력이 되는지 검증한다.
모의 객체는 특정 입력값에 대해서 수행되어야하는 동작이 정상적으로 동작했는지 검증한다.
따라서 위 두 가지 테스트 대역은 양자택일하는 것이 아니라, 두 가지 대역 모두 상호보완적으로 작성되어야 한다.
...
List<Long> stockIds = List.of(1L, 4L);
// stub: id가 1인 stockPrice는 1000
when(mockStockRepository.findById(1L)).thenReturn(1000D);
// stub: id가 4인 stockPrice는 5000
when(mockStockRepository.findById(4L)).thenReturn(5000D);
StockService stockService = new StockService(mockStockClient);
double totalPrice = stockService.retrieveTotalStockPrice(stockIds);
// stub을 사용한 테스트 (결과 검증)
assertEquals(6000D, totalPrice);
// mock을 사용한 테스트 (동작 검증)
verify(mockStockClient, times(2)).retrievePrice();
...
위 주석을 참고해보자. @Mock 어노테이션으로 만든 모의 객체를 mock의 용도로도 사용하지만 stub의 용도로도 사용한다.
그렇다면 @Mock으로 생성한 객체인데(@Stub이 아닌데) stub의 역할을 함께 수행하는 것일까?
(이 부분이 JUnit in action이라는 책을 읽으면서 가장 혼란스러운 부분이었다..)
답을 요약하자면 Mockito에서 굳이 이 둘을 분리할 필요를 못느껴서이다.
하나의 객체가 두 가지 역할을 모두 수행하도록하여, stubbing과 mocking을 위한 코드를 두 번 작성하지 않게 하는 의도였다.
이에 대한 아래 링크를 참고해보자.
stackoverflow: https://stackoverflow.com/questions/5261194/what-is-the-difference-between-mock-and-stub-when-using-mockito
https://semaphore.io/community/tutorials/stubbing-and-mocking-with-mockito-2-and-junit
Stubbing and Mocking with Mockito and JUnit - Semaphore Tutorial
Create true unit tests by mocking all external dependencies in your JUnit classes with the help of Mockito.
semaphore.io
'공부 > 테스트' 카테고리의 다른 글
테스트 코드의 장점 활용하기 (0) | 2025.04.16 |
---|---|
테스트의 종류와 필요성 (0) | 2025.04.13 |