개발을 하면서 테스트 코드를 작성할 때, Mock이라는 단어를 많이 접하게 됩니다.
이 Mock은 테스트 더블(Test Double)의 일종인데, 테스트하려는 객체와 연관된 객체를 사용하기가 어렵고 모호할 때 대신해줄 수 있는 객체를 테스트 더블이라고 합니다.
영화 촬영 시 위험한 역할을 대신하는 스턴트 더블에서 비롯된 단어.
테스트 더블에는 여러 종류가 있는데, 그 중에서 Mock과 Stub에 대해서 알아보겠습니다.
Stub
Stub은 테스트 중에 만들어진 호출에 대해 미리 준비된 답변을 제공하는 객체입니다.
테스트 대상 객체(SUT, System Under Test)가 정상적으로 동작하기 위해 필요한 의존성 객체를 단순하게 흉내냅니다.
- 상태 검증에 사용
- 테스트에서 호출되었을 때, 미리 정해진 값이나 상태를 반환하도록 구현
- 실제 로직을 거의 가지고 있지 않으며, 특정 값을 반환하는데 목적이 있음
Mock
Mock은 행위 검증(Behavior Verification)을 위해 사용되는 객체입니다.
테스트 대상 객체로부터 예상된 상호작용을 명세하고, 해당 상호작용이 올바르게 일어났는지 검증하는데 중점을 둡니다.
- 행위 검증에 사용
- 테스트가 실행된 후, 특정 메서드가 예상된 횟수만큼, 혹은 예상된 파라미터로 호출되었는지 검증
Stub의 기능을 포함할 수 있음 (특정 값을 반환하면서, 호출 여부도 검증 가능)
비교
| 구분 | Stub (스텁) | Mock (목) |
|---|---|---|
| 목적 | 테스트가 원활히 실행되도록 의존성을 대체 | 메서드 호출과 같은 행위가 올바르게 일어났는지 검증 |
| 검증 대상 | 상태 검증 : 테스트 완료 후 객체의 상태를 검증 | 행위 검증 : 객체 간의 상호작용(메서드 호출 등)을 검증 |
| 구현 | 주로 미리 정의된 데이터를 반환 | 호출에 대한 기대를 명세하고,행위를 검증하는 로직 포함 |
| 테스트 결합도 | 구현 변경에 덜 민감하여 결합도가 상대적으로 낮음 | 내부 구현(호출 방식)이 바뀌면 테스트가 깨지기 쉬워 결합도가 높음 |
Stub 사용 예제
getProduct(id) 메서드를 테스트하는 상황을 가정합니다. 이 테스트의 목표는 productRepository가 특정 상품을 잘 반환했을 때, productService가 그 상품 정보를 ProductResponse로 올바르게 변환해서 반환하는지(상태) 검증하는 것입니다.
- 시나리오:
productRepository.findById(1L)가 호출되면, 미리 정의된Product객체를 반환하도록 설정(Stub). - 검증:
productService.getProduct(1L)의 반환값이 예상과 일치하는지 확인.
import org.assertj.core.api.Assertions.assertThat
import org.mockito.kotlin.any
import org.mockito.kotlin.whenever
import java.util.Optional
// ... (클래스 선언부)
@Test
fun `[상태 검증] getProduct - 상품 ID로 조회 시, 상품 정보를 올바르게 반환한다`() {
// given (준비)
val productId = 1L
val sampleProduct = Product(name = "Macallan 18", price = 500000, quantity = 10).apply { id = productId }
// productRepository.findById()가 호출되면, 미리 준비된 sampleProduct를 반환하도록 설정 (Stub 역할)
whenever(productRepository.findById(any())).thenReturn(Optional.of(sampleProduct))
// when (실행)
val resultResponse = productService.getProduct(productId)
// then (검증) - 반환된 객체의 "상태"를 검증
assertThat(resultResponse.id).isEqualTo(productId)
assertThat(resultResponse.name).isEqualTo("Macallan 18")
assertThat(resultResponse.price).isEqualTo(500000)
}
Mock 사용 예제
registerProduct 메서드를 테스트하는 상황을 가정합니다. 이 테스트의 목표는 productService.registerProduct가 호출되었을 때, 내부적으로 productRepository.save() 메서드를 정상적으로 호출하는지(행위) 검증하는 것입니다.
- 시나리오:
productService.registerProduct를 특정 요청과 함께 호출. - 검증:
productRepository.save()메서드가 정확히 1번 호출되었는지 확인.
import org.mockito.kotlin.verify
import org.mockito.kotlin.times
// ... (클래스 선언부)
@Test
fun `[행위 검증] registerProduct - 상품 등록 요청 시, repository의 save 메서드를 1회 호출한다`() {
// given (준비)
val request = ProductRequest(name = "Macallan 12", price = 150000, quantity = 20)
val sampleProduct = Product(name = request.name, price = request.price, quantity = request.quantity)
// productRepository.save()가 호출되면 특정 객체를 반환하도록 설정 (Stub의 기능도 겸함)
whenever(productRepository.save(any())).thenReturn(sampleProduct.apply { id = 1L })
// when (실행)
productService.registerProduct(request)
// then (검증) - repository의 save 메서드가 Product 타입의 어떤 객체로든 "1번 호출"되었는지 "행위"를 검증
verify(productRepository, times(1)).save(any<Product>())
}
정리
- Stub : "테스트를 위해 이런 상태를 만들어 줘."
- Mock : "이런 행위를 했는지 알려줘."
일반적으로 테스트는 외부 의존성의 상태에 따라 결과가 달라지는 경우가 많으므로 Stub을 더 자주 사용하게 됩니다.
Mock은 외부 시스템에 실제로 영향을 주는 동작(결제 API 호출, DB 저장 / 삭제 등)이 반드시 실행되어야 하는지 확인할 때 유용합니다.
'Development' 카테고리의 다른 글
| APM (Application Performance Monitoring) (0) | 2025.09.04 |
|---|---|
| Forward Proxy, Reverse Proxy 정의와 차이점 (1) | 2025.08.26 |
| [Redis] 레디스를 로컬이 아닌 외부에 두는 이유 (0) | 2025.03.21 |
| [JPA] Spring Data JPA는 어떻게 새로운 Entity인지 알아내는걸까? (0) | 2025.01.22 |
| [Server] RAID 란? (0) | 2025.01.20 |
댓글