모킹과 단위 테스트

단위 테스트를 설명할 때는 보통 먼저 통합 테스트와 비교해서 보는 것이 이해가 쉽다.
통합 테스트는 말 그대로 두 개 이상의 클래스나 컴포넌트가 함께 협력하는 상태를 검증하는 테스트다.
즉, 내가 지금 개발 중인 코드만 따로 보는 것이 아니라,
그 코드가 의존하고 있는 다른 요소들까지 실제로 함께 실행하면서 동작을 확인하는 방식이다.
예를 들어 SimpleProductService가 ProductRepository와 함께 동작하는 상태에서 상품 추가와 조회를 테스트했다면,
그건 서비스 하나만 보는 것이 아니라 서비스와 저장소의 협력까지 포함해서 본 것이므로 통합 테스트라고 볼 수 있다.
반면 단위 테스트는 훨씬 더 범위를 좁게 본다.
단위 테스트는 오직 하나의 클래스, 또는 하나의 컴포넌트만을 검증하는 테스트를 의미한다.
즉, 어떤 클래스가 다른 클래스에 의존하고 있더라도, 그 의존 대상까지 실제로 함께 실행하는 것이 아니라
“지금 내가 검증하고 싶은 대상 하나”에만 집중하는 방식이다.
이번 편에서 다루는 핵심은, SimpleProductService를 테스트할 때
매번 실제 ProductRepository까지 같이 사용할 필요가 있을까 하는 질문이다.
서비스의 로직 자체만 보고 싶다면, 저장소 구현체까지 실제로 함께 동작시키는 것은 오히려 범위를 넓혀버리는 일이 될 수 있다.
이 지점에서 단위 테스트와 모킹이 필요한 이유가 자연스럽게 드러난다.
모킹을 통해서 SimpleProductService를 단위 테스트하는 코드를 작성해보기
SimpleProductService를 단위 테스트하기 위해 ProductRepository를 사용하지 않도록
코드를 직접 바꾸는 방법을 생각해볼 수 있다.
예를 들어 원래는 productRepository.add(product)를 호출하던 부분을,
테스트를 위해 임시로 Product savedProduct = product; savedProduct.setId(1L); 같은 형태로 바꿔버리는 방식이다.
그러면 저장소 없이도 서비스 메서드 자체는 실행해볼 수 있다. 즉, 기술적으로는 가능한 방법이다.
하지만 이 방식에는 아주 큰 문제가 있다.
오직 테스트를 위해 실제 애플리케이션 로직을 수정해야 한다는 점이다.
서비스를 단위 테스트할 때마다 저장소를 사용하는 부분을 손으로 제거하거나 바꿔야 한다면,
테스트를 위해 본래 코드가 오염되는 셈이다.
그리고 테스트가 끝난 뒤 다시 원래 코드로 되돌려야 할 수도 있다.
이 과정은 번거롭기도 하고, 무엇보다 실수의 여지가 너무 크다.
테스트용으로 잠깐 바꿔놓은 코드를 되돌리지 못하면, 그 자체가 버그나 장애로 이어질 수 있다.
모킹은 특정 클래스나 메서드가 하는 역할을 테스트 코드 안에서 가짜로 흉내 내는 것이다.
즉, 실제 ProductRepository를 호출하지 않더라도, 마치 저장소가 동작한 것처럼 보이게 테스트 상황을 만들어주는 것이다.
이 덕분에 서비스는 여전히 ProductRepository에 의존하는 구조 를 유지하면서도,
테스트에서는 그 의존 대상을 실제 구현이 아닌 “가짜 객체”로 대체할 수 있다.
자바에서 이런 모킹을 할 때 많이 사용하는 라이브러리가 Mockito다.
Mockito를 사용하면 “이 메서드가 호출되면 이런 값을 돌려줘” 같은 동작을 테스트 코드 안에서 정의할 수 있다.
예를 들어 SimpleProductService의 add() 메서드는 내부적으로 대략 이런 흐름을 가진다.
① ProductDto를 Product로 변환한다.
② 유효성 검사를 수행한다.
③ productRepository.add(product)를 호출한다.
④ 저장 결과를 다시 ProductDto로 변환한다.
단위 테스트에서 정말 보고 싶은 것은 이 흐름이 “서비스 로직 기준으로 올바르게 이어지는가”이지,
실제 데이터베이스 저장소가 동작하는지까지 매번 함께 검증하는 것은 아닐 수 있다.
그래서 저장소는 Mockito가 흉내 내고, 서비스는 그 가짜 저장소를 믿고 로직을 수행하게 만드는 것이다.
SimpleProductServiceUnitTest.java 파일 생성
@ExtendWith(MockitoExtension.class)
public class SimpleProductServiceUnitTest {
@Mock
private ProductRepository productRepository;
@Mock
private ValidationService validationService;
@InjectMocks
private SimpleProductService simpleProductService;
@Test
@DisplayName("상품 추가 후에는 추가된 상품이 변경되어야 한다. ")
void productAddTest() {
ProductDto productDto = new ProductDto("연필", 300, 20);
Long PRODUCT_ID = 1L;
Product product = ProductDto.toEntity(productDto);
product.setId(PRODUCT_ID);
when(productRepository.add(any())).thenReturn(product);
ProductDto savedProductDto = simpleProductService.add(productDto);
assertTrue(savedProductDto.getId().equals(PRODUCT_ID));
assertTrue(savedProductDto.getName().equals(productDto.getName()));
assertTrue(savedProductDto.getPrice().equals(productDto.getPrice()));
assertTrue(savedProductDto.getAmount().equals(productDto.getAmount()));
}
}
먼저 클래스 위의 @ExtendWith(MockitoExtension.class)는
이 테스트가 Mockito 기능을 사용하도록 설정하는 역할을 한다.
이 어노테이션을 붙이면 스프링 부트 애플리케이션 전체를 띄우지 않아도
Mockito가 필요한 mock 객체들을 생성해준다.
즉, 이 테스트는 @SpringBootTest처럼 애플리케이션을 통째로 실행하는 방식이 아니라, 훨씬 가볍고 빠르게 동작한다.
@SpringBootTest는 스프링 부트 애플리케이션을 실제로 띄우면서 빈 등록과 주입 과정을 거치지만,
Mockito 기반 단위 테스트는 그런 전체 실행 없이도 테스트를 수행할 수 있다.
@Mock은 가짜 객체를 만드는 어노테이션이다.
여기서는 ProductRepository와 ValidationService가 mock 객체로 만들어진다.
@InjectMocks는 mock 객체들을 주입해서 테스트 대상 객체를 만들어주는 역할을 한다.
즉, SimpleProductService는 실제 ProductRepository가 아니라
Mockito가 만든 productRepository mock을 주입받아 생성된다.
이렇게 되면 서비스는 평소처럼 의존성을 가진 상태로 동작하지만,
테스트에서는 외부 의존성이 모두 가짜 객체로 대체되어 있는 셈이다.
그다음 핵심 구문이 when(productRepository.add(any())).thenReturn(product);이다.
이 코드는 “productRepository.add(...)가 어떤 값으로 호출되든, 결과로는 product를 반환해라”는 뜻이다.
즉, 저장소가 실제로 insert를 수행하는 대신, 테스트 안에서는 미리 만들어둔 product를 결과로 돌려주게 만든다.
이 product에는 id가 1L로 들어가 있으므로,
서비스 입장에서는 마치 저장소가 저장 후 생성된 id를 넣어 반환해준 것처럼 보이게 된다.
이후 simpleProductService.add(productDto)를 호출하면, 서비스는 내부적으로 DTO를 엔티티로 바꾸고,
검증을 수행하고, 저장소에 저장하고, 다시 DTO로 바꾸는 자기 로직을 그대로 수행한다.
다만 여기서 저장소 부분만 실제 구현이 아니라 mock으로 대체된 것이다.
마지막 assertTrue(...) 들은 저장 후 반환된 DTO가 우리가 기대한 id와 이름, 가격, 재고 수량을 가지고 있는지 확인한다.
즉, 이 테스트는 저장소가 실제로 동작하는지를 보는 것이 아니라,
저장소가 이런 값을 돌려줬다고 가정했을 때 서비스가 올바른 결과를 만들어내는지를 검증하는 테스트라고 볼 수 있다.
'공부 기록 > 이것이 백엔드 개발이다 With Java' 카테고리의 다른 글
| EP24. 상품 관리 애플리케이션에 객체지향 더하기 2편 (0) | 2026.04.26 |
|---|---|
| EP23. 상품 관리 애플리케이션에 객체지향 더하기 1편 (0) | 2026.04.25 |
| EP22. 상품 관리 애플리케이션에 데이터베이스 연동하기 3편 (0) | 2026.04.24 |
| EP21. 상품 관리 애플리케이션에 데이터베이스 연동하기 2편 (0) | 2026.04.23 |
| EP20. 상품 관리 애플리케이션에 데이터베이스 연동하기 1편 (0) | 2026.04.22 |