(Kotlin/코틀린) MockK 심화 — spy, slot, captureArgument


1. spy — 실제 객체를 부분적으로 모킹

mock은 모든 함수가 가짜로 동작하지만, spy는 실제 객체를 감싸서 특정 함수만 선택적으로 재정의합니다.

import io.mockk.spyk
import io.mockk.every

class Calculator {
    fun add(a: Int, b: Int) = a + b
    fun multiply(a: Int, b: Int) = a * b
    fun complexOperation(a: Int, b: Int) = add(a, b) * multiply(a, b)
}

@Test
fun `spy는 재정의하지 않은 함수는 실제 로직을 수행`() {
    // spyk: 실제 Calculator 인스턴스를 감싼 spy 객체 생성
    val calculatorSpy = spyk(Calculator())

    // add()만 가짜 값으로 재정의 — 나머지 함수는 실제 구현 그대로 동작
    every { calculatorSpy.add(2, 3) } returns 100

    assertEquals(100, calculatorSpy.add(2, 3))       // 스터빙된 값
    assertEquals(6,   calculatorSpy.multiply(2, 3))  // 실제 로직 그대로 (재정의 안 함)

    // complexOperation 내부에서 add()를 호출 → 스터빙된 100이 사용됨
    // 100 * 6 = 600 (실제 add였다면 5 * 6 = 30)
    assertEquals(600, calculatorSpy.complexOperation(2, 3))
}

mock vs spy: mock은 완전히 가짜 객체(모든 함수가 스터빙 필요), spy는 실제 객체를 기반으로 일부만 가짜로 바꿉니다. 레거시 클래스의 특정 부분만 격리하고 싶을 때 유용합니다.


2. slot — 전달된 인자 캡처

함수에 전달된 실제 인자값을 검증하고 싶을 때 slot을 사용합니다.

import io.mockk.slot
import io.mockk.CapturingSlot

@Test
fun `저장 시 전달된 객체의 내용을 검증한다`() {
    val mockRepository = mockk<UserRepository>()
    val userSlot = slot<User>()  // User 타입 인자를 담을 슬롯

    // capture(slot): save()에 전달된 인자를 slot에 저장하도록 설정
    every { mockRepository.save(capture(userSlot)) } just Runs

    val userService = UserService(mockRepository)
    userService.registerUser(name = "홍길동", email = "hong@example.com")

    // slot.captured: 실제로 전달된 인자값에 접근
    val savedUser = userSlot.captured
    assertEquals("홍길동", savedUser.name)
    assertEquals("hong@example.com", savedUser.email)
    assertNotNull(savedUser.id)  // registerUser 내부에서 자동 생성된 ID까지 검증 가능
}

verify(match {})와의 차이: 단순 비교라면 verify { mockRepository.save(match { it.name == "홍길동" }) }로도 충분합니다. slot은 캡처한 객체를 여러 필드에 걸쳐 상세히 검증하거나, 이후 로직에서 재사용하고 싶을 때 유리합니다.


3. mutableListOf + capture — 여러 번 호출된 인자 모두 캡처

import io.mockk.mutableListOf

@Test
fun `여러 번 호출된 알림 내용을 모두 검증한다`() {
    val mockNotifier = mockk<NotificationService>()
    val capturedMessages = mutableListOf<String>()

    // capture(list): 호출될 때마다 인자가 리스트에 순서대로 누적됨
    every { mockNotifier.send(any(), capture(capturedMessages)) } just Runs

    val batchService = BatchNotificationService(mockNotifier)
    batchService.notifyAll(
        userIds = listOf("u1", "u2", "u3"),
        template = "안녕하세요, {name}님"
    )

    // 3번 호출되었고, 각 호출의 메시지가 순서대로 저장됨
    assertEquals(3, capturedMessages.size)
    assertTrue(capturedMessages[0].contains("u1") || capturedMessages[0].contains("안녕"))
}

4. verify 순서 검증 — verifyOrder / verifySequence

여러 함수가 정확한 순서로 호출되었는지 검증해야 할 때 사용합니다.

import io.mockk.verifyOrder
import io.mockk.verifySequence

class PaymentServiceTest {
    private val mockValidator = mockk<PaymentValidator>(relaxed = true)
    private val mockGateway = mockk<PaymentGateway>(relaxed = true)
    private val mockLogger = mockk<Logger>(relaxed = true)

    @Test
    fun `결제는 검증 후 처리되고 마지막에 로그가 남는다`() {
        val service = PaymentService(mockValidator, mockGateway, mockLogger)

        service.processPayment(amount = 10000)

        // verifyOrder: 지정한 호출들이 "이 순서대로" 일어났는지 검증
        // (다른 호출이 중간에 끼어 있어도 상대적 순서만 맞으면 통과)
        verifyOrder {
            mockValidator.validate(10000)
            mockGateway.charge(10000)
            mockLogger.log("결제 완료: 10000원")
        }
    }

    @Test
    fun `정확히 이 순서로만 호출되어야 한다`() {
        val service = PaymentService(mockValidator, mockGateway, mockLogger)
        service.processPayment(amount = 5000)

        // verifySequence: 이 세 호출이 "다른 호출 없이 연속으로" 정확히 이 순서로만 발생
        verifySequence {
            mockValidator.validate(5000)
            mockGateway.charge(5000)
            mockLogger.log("결제 완료: 5000원")
        }
    }
}

verifyOrder vs verifySequence: verifyOrder는 상대적 순서만 확인(중간에 다른 호출 허용), verifySequence는 지정한 호출들만 정확히 그 순서로 전체를 구성해야 통과합니다.


5. object / 최상위 함수 모킹 — mockkObject, mockkStatic

Kotlin 고유의 object 싱글턴이나 최상위 함수도 MockK로 모킹할 수 있습니다 (Mockito에서는 어려운 영역입니다).

import io.mockk.mockkObject
import io.mockk.unmockkObject
import io.mockk.mockkStatic

// 싱글턴 object
object DateProvider {
    fun today(): LocalDate = LocalDate.now()
}

@Test
fun `object 싱글턴의 함수를 모킹한다`() {
    // mockkObject: 해당 object의 함수 호출을 가로챌 수 있게 설정
    mockkObject(DateProvider)
    every { DateProvider.today() } returns LocalDate.of(2026, 1, 1)

    val result = DateProvider.today()
    assertEquals(LocalDate.of(2026, 1, 1), result)

    // 테스트 후 원래 상태로 복구 (다른 테스트에 영향 방지 — 필수!)
    unmockkObject(DateProvider)
}

@Test
fun `최상위 확장 함수를 모킹한다`() {
    // mockkStatic: 특정 클래스에 대한 확장 함수(최상위 함수)를 모킹
    mockkStatic("kotlin.io.FilesKt")

    val mockFile = mockk<File>()
    every { mockFile.readText() } returns "테스트 내용"

    assertEquals("테스트 내용", mockFile.readText())

    unmockkStatic("kotlin.io.FilesKt")
}

object/static 모킹 주의사항: 전역 상태를 변경하므로 반드시 unmockkObject/unmockkStatic으로 복구해야 합니다. @After에 unmockkAll()을 넣어 모든 테스트 후 자동 정리하는 것이 안전합니다.

@After
fun tearDown() {
    unmockkAll()  // 이 테스트 클래스에서 사용한 모든 mock/spy/object 모킹 초기화
}

6. 실전 예제 — 복합 시나리오

class OrderServiceTest {
    private val mockRepository = mockk<OrderRepository>()
    private val mockPaymentGateway = mockk<PaymentGateway>()
    private val orderSlot = slot<Order>()

    @Test
    fun `주문 생성 시 올바른 상태로 저장되고 결제가 순서대로 처리된다`() {
        every { mockRepository.save(capture(orderSlot)) } just Runs
        every { mockPaymentGateway.charge(any()) } returns PaymentResult.Success

        val service = OrderService(mockRepository, mockPaymentGateway)
        service.createOrder(productId = "P001", amount = 30000)

        // 캡처한 인자로 저장 내용 검증
        assertEquals("P001", orderSlot.captured.productId)
        assertEquals(OrderStatus.PENDING, orderSlot.captured.status)

        // 호출 순서 검증: 저장 먼저, 결제는 그 다음
        verifyOrder {
            mockRepository.save(any())
            mockPaymentGateway.charge(30000)
        }
    }
}

7. 정리

API 용도
spyk(실제객체) 실제 객체 기반, 일부 함수만 재정의
slot<T>() + capture() 전달된 인자값을 캡처해 상세 검증
mutableListOf + capture() 여러 번 호출된 인자를 모두 캡처
verifyOrder { } 상대적 호출 순서 검증
verifySequence { } 정확한 전체 호출 순서 검증
mockkObject / mockkStatic Kotlin object, 최상위 함수 모킹
unmockkAll() 모든 모킹 상태 초기화 (테스트 후 필수)


Related Posts