본문 바로가기
프로그래밍/테스트

[mockk] 코틀린 테스트 프레임워크에 대해서 알아보자

by 사바라다 2021. 9. 6.

안녕하세요. 오늘은 코틀린 테스트 프레임워크인 mockk의 사용법에 대해서 알아보는 시간을 가져보도록 하겠습니다.

mockk framework

mockk는 코틀린 스타일로 테스트 코드를 작성할 수 있도록 도와주는 라이브러리입니다. 기존의 java에서 사용하시던 mockkito를 대체한다고 보시면 됩니다. mockk를 사용하기 위해서는 아래처럼 mockk에 대한 의존성을 주입해주실 필요가 있습니다. 포스팅을 쓰는 시점의 가장 최신 버전은 1.12.0 이므로 저는 이 버전을 사용하도록 하겠습니다.

testImplementation("io.mockk:mockk:1.12.0")

테스트 서비스 예제 코드

mockk로 테스트를 만드는데 사용할 코드는 아래와 같습니다. 메인으로 테스트할 코드는 마지막에 있는 MappingService 코드입니다. MappingService 코드에는 테스트용으로 approve 함수를 만들어두었습니다. 해당 함수는 들어오는 값이 타당한지 판단 후 entity를 가져오고 isApprove의 값을 true로 변경한 뒤 다시 저장을 하는 로직을 가지고 있습니다.

@Entity
data class Mapping(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null,
    private val senderType: UserType? = null,
    private val senderId: Long? = null,
    private val receiverType: UserType? = null,
    private val receiverId: Long? = null,
    private var isApprove: Boolean? = false
) : BaseEntityModel() {

    fun approve() {
        isApprove = true
    }
}
interface MappingRepository : JpaRepository<Mapping, Long>
@Service
class MappingService(
    private val mappingRepository: MappingRepository
) {

    @Transactional
    fun approve(id: Long) {

        validate(id)

        val mapping = mappingRepository.findById(id)
            .orElseThrow { NullPointerException() }

        mapping.approve()

        mappingRepository.saveAndFlush(mapping)
    }

    private fun validate(id: Long) {
        if (id <= 0) {
            throw IllegalArgumentException()
        }
    }
}

아래 코드는 mockk를 이용하여 위 서비스 코드의 approve 메서드 코드를 테스트한 코드입니다. 아래 코드는 mockk를 이용하여 mock과 stub을 생성, verify를 이용하여 호출 횟수를 검증하였습니다. 또한 private 메서드 또한 mocking하여 호출 횟수 및 정상적인 호출을 확인하였습니다. 그리고 내부에서 사용한 호출 파라미터를 capture를 이용하여 확인하였습니다. 오늘 포스팅에서 이러한 내용을 하나하나 알아보도록 하겠습니다.

@Test
fun `approve 코드 테스트`() {
    val id = 2L
    val mappingRepository: MappingRepository = mockk()
    val mappingService: MappingService = spyk(
        objToCopy = MappingService(mappingRepository),
        recordPrivateCalls = true
    )
    val slot: CapturingSlot<Mapping> = slot()

    every { mappingService["validate"](id) } returns Unit

    every { mappingRepository.findById(id) } returns Optional.of(
        Mapping(1L, UserType.A, 2L, UserType.U, 3L, false)
    )

    every { mappingRepository.saveAndFlush(capture(slot)) } answers { slot.captured }

    mappingService.approve(2L)

    verify(exactly = 1) { mappingService["validate"](id) }
    verify(exactly = 1) { mappingRepository.findById(id) }
    verify(exactly = 1) { mappingRepository.saveAndFlush(slot.captured) }
    Assertions.assertEquals(slot.captured.id, 1L)
}

기본적인 mock & stub

가장 유닛 테스트를 만들기 위해서 객체를 외부의존성에 대해서 통제가능한 선으로 만들어야합니다. 그러기 위해서 하는 행위를 Mocking이라고 하는데요. 이 실제 외부 객체를 주입하는것이 아닌 dummy 객체를 호출하게 합니다. 그리고 이 dummy 객체의 응답 값 등을 통제하여 원하는 코드의 테스트를 진행하는 방법입니다.

mockk를 사용해서 mocking하기 위해서는 아래와 같이 mockk() 를 이용할 수 있습니다. 아래의 코드는 MappingRepository를 mocking 하여 이 값을 MappingService에 주입하여 객체를 생성하고 있습니다. 이렇게 생성하면 우리는 mappingRepository의 응답값 역시 실제 DB를 호출하지 않고 원하는 응답값으로 만들어내어 사용할 수 있게됩니다.

@Test
fun test() {
    val mappingRepository: MappingRepository = mockk()
    val mappingService = MappingService(mappingRepository)
}

또는 아래와 같은 방법으로도 선언할 수 있습니다.

@ExtendWith(MockKExtension::class)
internal class MappingServiceTest {

    @MockK
    lateinit var mappingRepository: MappingRepository

    @Test
    fun test() {
        val mappingService = MappingService(mappingRepository)
    }
}

mock 객체로 만들어 호출하면 응답값을 설정할 수 있습니다. 이때는 every { [함수 호출 코드] } returns [값] 문법을 사용하면 정의할 수 있습니다. 이러한 응답값을 설정하는것을 stub 이라고 합니다. 아래의 코드 예제는 mappingRepository.count()를 호출하면 반드시 100을 반환한다는 예제입니다. everyreturns 문법을 이용하여 반환 값을 정의한것을 알 수 있습니다. 해당 코드는 실제 DB를 조회하지 않고 바로 반환되는 값입니다. return이 아니라 returns 라는 것은 주의하셔야되는 점입니다.

@Test
fun test() {
    val mappingRepository: MappingRepository = mockk()

    every { mappingRepository.count() } returns 100

    println("mappingRepository.count() = ${mappingRepository.count()}") // mappingRepository.count() = 100
}

verify

verify는 메서드가 테스트 안에서 정상적으로 호출 되었는지를 검증할 때 사용하는 키워드입니다. mockk를 이용하면 verify도 코틀린 스럽게 작성할 수 있습니다. verify { [함수 콜] }가 기본적인 사용방법입니다. 아래의 코드를 예제로 보시면 mappingRepository.count()가 호출됨을 검증하는 코드로 볼 수 있습니다. 이를 통해 우리는 이 테스트 코드에서는 mappingRepository.count()가 호출되는 구나라고 확실히 알 수 있는 것입니다.

@Test
fun test() {
    val mappingRepository: MappingRepository = mockk()

    every { mappingRepository.count() } returns 100

    verify { mappingRepository.count() }
}

정확하게 몇번 호출되는지 이런 조건을 달아줄 수 있습니다. 이 역시 코틀린 문법에 맞게 작성할 수 있습니다. 아래 코드를 보시면 알 수 있습니다. 아래 코드는 verifyexactly 파리미터를 이용하고 있습니다. 아래코드는 1번만 호출 된다는 것을 검증하는 코드입니다.

@Test
fun test() {
    val mappingRepository: MappingRepository = mockk()

    every { mappingRepository.count() } returns 100

    verify(exactly = 1) { mappingRepository.count() }
}

private function mocking / dynamic calls

테스트 코드를 작성하다보면 private function에 대해서 mocking과 stubing을 진행해야할 경우가 있습니다. 이럴경우에 사용할 수 있는 방법이 mockk에서 제공하는 dynamic call입니다. mockk에서 dynamic call을 사용하는 코드를 예제로 가져왔습니다. dynamic call을 사용하기 위해서는 먼저 호출 객체를 spyk로 spy 객체로 만들어야합니다. spy 객체란 실제 객체의 코드를 사용하지만 every를 이용하여 stub을 한 메서드에 한해서만 내부 로직을 실행하지 않고 정해진 응답을 반환할 수 있도록 하는 테스트 방법입니다. 그리고 private 함수의 mock & stub은 mappingService["validate"](2L)라는 형식으로 가능합니다.

@Test
fun test() {
    val mappingRepository: MappingRepository = mockk()
    val mappingService: MappingService = spyk(
        objToCopy = MappingService(mappingRepository),
        recordPrivateCalls = true
    )

    every { mappingService["validate"](2L) } returns Unit // private function mock & stub 
    every { mappingRepository.findById(2L) } returns Optional.empty()

    mappingService.approve(2L)
}

capturing

capturing은 mock 또는 spy 객체에 대해서 함수에 들어가는 파라미터의 값을 가져와서 검증하기 위해서 사용하는 방법입니다. mockk에서는 every 또는 verify 에 caputre()를 통해서 slot에 값을 저장할 수 있고 이를 slot.captured을 통해서 가져올 수 있습니다. 아래의 코드에서는 verify에서 mappingRepository.save(capture(slot))를 통해서 save의 파라미터로 들어가는 값이 어떤 값인지 capture()를 통해서 slot에 저장한 후 가져와서 값을 비교하는 것 입니다.

@Test
fun test() {
    val mappingRepository: MappingRepository = mockk()
    val slot: CapturingSlot<Mapping> = slot() // capture 준비

    val mapping = Mapping(1L, UserType.A, 2L, UserType.U, 3L, false)

    every { mappingRepository.save(mapping) } returns mapping

    mappingRepository.save(mapping)

    verify(exactly = 1) { mappingRepository.save(capture(slot)) } // save의 파라미터를 캡쳐
    val captured = slot.captured // 캡쳐된 파라미터를 가져오기

    Assertions.assertEquals(captured.id, 1L) // 내용 확인
}

마무리

오늘은 이렇게 mockk를 사용하는 방법에 대해서 알아보았습니다.

이 외에도 mockk는 코루틴 테스트를 지원해준다고 합니다.

저도 아직 코루틴 테스트는 진행해보지 않았습니다. 관심있으신 분들은 공식사이트를 참고 부탁드리겠습니다 !

감사합니다.

참조

mockk

kotlin-academy-mocking-is-not-rocket-science-basics

댓글