이전글에서 kotest의
test style과 assertion에 대해 살펴봤다.
이번글에서는 kotest에서 mockk를 사용하여
모킹 후 테스트 하는 방법을 자세히 살펴보자.
mocking 처리를 위해서 기존에는 Mockito를 주로 이용했지만, 코틀린에서는 Mockk를 사용하는 것이 권장된다.
dependencies {
testImplementation("io.mockk:mockk:{$MOCKK_VERSION}")
}
아래 예제를 확인해보자.
interface PredictInterface {
fun predict(memberId: Long): Order
}
data class Order(
val orderId: Long
)
class PredictService(
private val predictInterface: PredictInterface
) {
fun predict(memberId: Long): Order {
return predictInterface.predict(memberId)
}
}
PredictService에는 고객이 고객센터로 문의하는 시점에, 기존에 주문 했던 주문 정보들 중 어떠한 주문에 대해서 문의 할지 미리 예측하는 서비스를 가지고 있다.
PredictInterface는 외부 api를 통해 제공된다고 가정하고, 다른 팀에서 아직 개발 중이라면 우리는 이를 모킹을 통하여 테스트를 진행해야 할 것이다.
아래와 같이 모킹을 하여 외부 api에 대해 예상 결과값을 지정(stub)하여 테스트를 할 수 있다.
internal class MainKtTest : BehaviorSpec({
// mock 객체 생성
val predictInterface = mockk<PredictInterface>()
val predictService = PredictService(predictInterface)
Given("테스트에 필요한 값 준비") {
val memberId = 1L
val resultOrderId = 3L
val resultOrder = Order(resultOrderId)
When("1번 회원이 문의 했을 경우 예상 문의 주문건을 반환한다.") {
// 외부 api에 대해 결과 값을 지정(stub)
every { predictInterface.predict(memberId) } returns resultOrder
// predict 실행
val order = predictService.predict(memberId)
Then("결과값 검증") {
order.orderId shouldBe resultOrderId
verify(exactly = 1) { predictInterface.predict(memberId) } // predict 메소드가 1번 호출 되었는지 확인
}
}
}
})
위와 같이 every, verify 등 다양한 모킹 및 검증 함수를 제공
한다.
Mock 객체를 생성 후 객체가 어떻게 동작할지 여러가지로 정의할 수 있다.
every { predictInterface.predict() } returns resultOrder // 주문 정보 리턴
every { predictInterface.predict() } throws Exception() // Exception 발생
every { predictInterface.predict() } just Runs // Unit 함수 실행
임의의 인자 값과 일치하도록 설정하려면 any()를 사용한다.
every { predictInterface.predict(any()) } returns resultOrder
verify는 메서드가 테스트 안에서 정상적으로 호출 되었는지를 검증할 때 사용하는 키워드 이다.
verify(atLeast = 3) { predictInterface.predict() }
verify(atMost = 2) { predictInterface.predict() }
verify(exactly = 1) { predictInterface.predict() }
verify(exactly = 0) { predictInterface.predict() }
every {…} 를 통해 매번 mock 처리를 하는 것은 번거로울 수 있다.
mock 대상이 많거나 특별히 확인할 내용이 없다면 더욱 그럴 수 있다. 이러한 경우에
relaxed mock을 이용하는 것이 좋다.
relaxed = true 옵션을 주게 되면 primitive 값들은 모두 0, false, "" 를
반환하게 된다.
또한 참조타입의 경우에는 chained mocks로 다시 relaxed mock 객체를 반환한다.
Given("테스트에 필요한 값 준비") {
val memberId = 1L
val resultOrder = mockk<Order>(relaxed = true)
When("1번 회원이 문의 했을 경우 예상 문의 주문건을 반환한다.") {
every { predictInterface.predict(memberId) } returns resultOrder
val order = predictService.predict(memberId)
Then("결과값 검증") {
verify(timeout = 1) { predictInterface.predict(memberId) }
order.orderId shouldBe resultOrder.orderId
}
}
}
capturing은 mock 또는 spy 객체에 대해서 함수에 들어가는 파라미터 값을 가져와서
검증하기 위해 사용하는 방법이다.
mockk에서는 slot과 capture 키워드를 이용하여 검증이 가능하다.
Given("테스트에 필요한 값 준비") {
val memberId = 1L
val argumentSlot = slot<Long>() // capture 준비
val resultOrder = mockk<Order>(relaxed = true)
When("1번 회원이 문의 했을 경우 예상 문의 주문건을 반환한다.") {
// predict 파라미터를 캡처
every { predictInterface.predict(capture(argumentSlot)) } returns resultOrder
val order = predictService.predict(memberId)
Then("결과값 검증") {
// 캡처 된 파라미터 가져오기
argumentSlot.captured shouldBe memberId
}
}
}
Reference
https://www.baeldung.com/kotlin/mockk
https://www.devkuma.com/docs/kotlin/kotlin-mockk-%EC%82%AC%EC%9A%A9%EB%B2%95/
https://kapentaz.github.io/test/Kotlin%EC%97%90%EC%84%9C-mock-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EA%B8%B0/#