val userRepository = mockk<UserRepository>()
// 특정 입력에 대한 반환
every { userRepository.findById(1) } returns User(id = 1, name = "홍길동")
// 모든 입력에 대해 동일 반환
every { userRepository.findAll() } returns listOf(
User(id = 1, name = "홍길동"),
User(id = 2, name = "김영희")
)
val orderService = mockk<OrderService>()
// any() - 모든 값 허용
every { orderService.calculateShippingFee(any()) } returns 5000
// 범위 지정 (more, less 등)
every {
orderService.applyDiscount(more(100000))
} returns 0.15 // 100,000원 이상 구매 시 15% 할인
// or() - 여러 값 중 하나
every {
orderService.isEligibleForPromotion(or(1, 2, 3))
} returns true // ID가 1, 2, 3인 고객만 프로모션 대상
val userService = mockk<UserService>()
every { userService.authenticateUser(any(), any()) } answers {
val email = firstArg<String>()
val password = secondArg<String>()
if (email == "admin@example.com" && password == "correct") {
AuthToken(token = "jwt-token-123", userId = 1)
} else {
throw AuthenticationException("인증 실패")
}
}
val token = userService.authenticateUser("admin@example.com", "correct")
// AuthToken(token = "jwt-token-123", userId = 1)
val apiClient = mockk<ApiClient>()
every { apiClient.retry() } returns Unit andThen {
throw TimeoutException("타임아웃")
} andThen {
Unit // 세 번째 시도 성공
}
apiClient.retry() // 성공
apiClient.retry() // 실패
apiClient.retry() // 성공
// ...
@Test
fun `이벤트 필터링이 정상 작동해야 한다`() {
val eventRepository = mockk<EventRepository>()
// 실제 EventService 인스턴스 생성, private 호출 기록
val eventService = spyk(EventService(eventRepository), recordPrivateCalls = true)
val events = listOf(
Event(id = 1, status = "ACTIVE", createdAt = LocalDateTime.now()),
Event(id = 2, status = "INACTIVE", createdAt = LocalDateTime.now().minusDays(1)),
Event(id = 3, status = "ACTIVE", createdAt = LocalDateTime.now().minusDays(2))
)
every { eventRepository.findAll() } returns events
// 실제 filterActiveEvents() 메서드 호출
val result = eventService.getActiveEvents()
// private 메서드 호출 검증
verify { eventService["filterByStatus"](events, "ACTIVE") }
result.size shouldBe 2
}
}
CapturingSlot로 호출된 인자값을 추출할 수 있다.// ...
@Test
fun `전송된 이메일 내용을 검증해야 한다`() {
val emailSlot = slot<Email>()
every { emailRepository.save(capture(emailSlot)) } returns Email(id = 1, to = "", subject = "")
// When
emailService.sendWelcomeEmail("newuser@example.com")
// Then - 실제로 저장된 Email 객체 내용 검증
val capturedEmail = emailSlot.captured
capturedEmail.to shouldBe "newuser@example.com"
capturedEmail.subject shouldContain "환영"
capturedEmail.body shouldContain "가입을 환영합니다"
}
}
MutableList로 여러 호출을 기록할 수도 있다.@Test
fun `여러 번의 로깅 호출을 모두 기록해야 한다`() {
val logMessages = mutableListOf<String>()
val logger = mockk<Logger>()
every { logger.info(capture(logMessages)) } just runs
// When
val userService = UserService(logger)
userService.registerUser("john@example.com")
userService.registerUser("jane@example.com")
// Then
logMessages.size shouldBe 2
logMessages[0] shouldContain "john@example.com"
logMessages[1] shouldContain "jane@example.com"
}
// 모듈 레벨 확장함수 (String.kt 파일에 정의)
fun String.isValidEmail(): Boolean {
return this.contains("@")
}
// 테스트
@Test
fun `확장함수 모킹`() {
val user = User("test@example.com")
// 모듈 전체를 mock 대상으로 지정
mockkStatic("com.example.StringKt")
every { user.email.isValidEmail() } returns false
val result = user.email.isValidEmail()
result shouldBe false
unmockkStatic("com.example.StringKt")
}
// Utils.kt
fun generateOrderId(): String = "ORD-${System.currentTimeMillis()}"
// 테스트
@Test
fun `탑레벨 함수 모킹`() {
mockkStatic(::generateOrderId)
every { generateOrderId() } returns "ORD-MOCK-001"
val orderId = generateOrderId()
orderId shouldBe "ORD-MOCK-001"
unmockkStatic(::generateOrderId)
}
// 정확히 3번 호출
verify(exactly = 3) { emailService.sendEmail(any(), any()) }
// 최소 2번 이상
verify(atLeast = 2) { emailService.sendEmail(any(), any()) }
// 최대 4번 이하
verify(atMost = 4) { emailService.sendEmail(any(), any()) }
// 0번 (호출 안 됨)
verify(exactly = 0) { emailService.sendEmail("never@example.com", any()) }
// ...
every { repository.findUser(any()) } returns User(id = 1, name = "홍길동")
every { cache.put(any(), any()) } just runs
// ...
// 특정 순서대로 호출되었는지 확인
verifySequence {
repository.findUser(1)
cache.put(1, any()) // repository 호출 후 cache 호출
}
// verifyOrder는 순서만 확인, 사이에 다른 호출 가능
verifyOrder {
repository.findUser(1)
cache.put(1, any())
}
private 함수 모킹을 위해선 다음 두 가지가 필요하다.
spyk로 실제 객체와 함께 사용recordPrivateCalls = true 옵션class Calculator {
fun add(a: Int, b: Int): Int = internalAdd(a, b)
private fun internalAdd(a: Int, b: Int): Int = a + b
}
@Test
fun `private 함수 모킹`() {
val calc = spyk(Calculator(), recordPrivateCalls = true)
every { calc["internalAdd"](any(), any()) } returns 100
val result = calc.add(5, 3)
result shouldBe 100
verify { calc["internalAdd"](5, 3) }
}
private 함수에 대한 모킹은 테스트에서 권장하지 않는다.private 함수를 모킹하거나 검증해야할 정도면 퍼블릭 함수 또는 별도 클래스로의 분리를 추천한다.// 문제: relaxed mock에서 generic 타입 ClassCastException
val service = mockk<GenericService<User>>(relaxed = true)
ClassCastException이 발생한다.// 해결: 명시적 stubbing
val service = mockk<GenericService<User>>()
every { service.get() } returns User(id = 1, name = "홍길동")
class OrderProcessor(private val baseDiscount: Double) {
fun applyDiscount(price: Double): Double = price * (1 - baseDiscount)
}
@Test
fun `특정 생성자만 모킹`() {
mockkConstructor(OrderProcessor::class)
// 매개변수 0.1인 생성자만 모킹
every {
constructedWith<OrderProcessor>(EqMatcher(0.1)).applyDiscount(any())
} returns 8000
val processor = OrderProcessor(0.1)
processor.applyDiscount(10000) shouldBe 8000
}
mockkConstructor - 지정한 클래스의 모든 생성자를 모킹 대상으로 지정constructedWith<Type>(EqMatcher(…)).applyDiscount(…) - EqMatcher에 전달된 파라미터 값인 경우에만 해당 메서드를 모킹
OrderProcessor에 대해서만 applyDiscount 메서드가 8000을 반환한다.