안녕하세요. 오늘은 코틀린 테스트 프레임워크인 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을 반환한다는 예제입니다. every
와 returns
문법을 이용하여 반환 값을 정의한것을 알 수 있습니다. 해당 코드는 실제 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() }
}
정확하게 몇번 호출되는지 이런 조건을 달아줄 수 있습니다. 이 역시 코틀린 문법에 맞게 작성할 수 있습니다. 아래 코드를 보시면 알 수 있습니다. 아래 코드는 verify
에 exactly
파리미터를 이용하고 있습니다. 아래코드는 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는 코루틴 테스트를 지원해준다고 합니다.
저도 아직 코루틴 테스트는 진행해보지 않았습니다. 관심있으신 분들은 공식사이트를 참고 부탁드리겠습니다 !
감사합니다.
참조
'기타 > 테스트' 카테고리의 다른 글
[kotlin] 레거시(legacy) 코드 리팩토링하기 2편 - 라인커버리지 100% 달성하기 (0) | 2022.04.23 |
---|---|
[kotlin] 레거시(legacy) 코드 리팩토링하기 1편 - 테스트 코드를 작성해야하는 이유 (0) | 2022.04.16 |
[spring + spock + TestContainer] Spring, Spock Framework에서 기능 테스트 하기 - TestContainer 사용 (0) | 2021.04.30 |
[UNIT-TEST] Webflux Reactor 유닛 테스트 하기 (0) | 2020.12.17 |
[Spock] Spock Framework 이용하기 - Where (0) | 2020.09.04 |
댓글