테스트 코드를 작성하면서 실제 DB와 연동하지 않고 Repository를 테스트 하기위해 Mockk을 사용해 검증했다.
또한 테스트 코드 작성중 불가피 하게 private 메소드를 검증 할 일이 생겼는데 그 방법을 정리 했다.
대부분 private 메소드를 테스트 하고 싶다는 건 책임 분리가 제대로 이루어 지지 않았다는 뜻이므로 최후의 수단으로 사용하자.
MockK는 코틀린을 위한 Mock 프레임워크이다.
자바에서의 Mockkito와 비슷한 역할을 한다.
Repository, Api호출 메소드와 같이 외부 의존성을 가지는 객체를 mocking 하여 결합도를 낮출 수 있습니다.
// 1. mockk<타입지정>()
private val entityRepository1 = mockk<EntityRepository1>()
// 2. 타입 추론 이용
private val entityRepository2 : EntityRepository2 = mockk()
아래와 같은 엔티티 하나가 주입된 서비스가 있다고 가정한다.
엔티티는 count를 가지고 있고, update시 count가 10 증가한다.
@Service
class EntityService(
private val entityRepository: EntityRepository
) {
@Transactional
fun updateCount(id: Long) {
validate(id)
val entity = entityRepository.findById(id)
.orElseThrow { NotFoundException() }
entity.count = entity.count + 10
entityRepository.save(entity)
}
private fun validate(id: Long) {
if (id <= 0) {
throw IllegalArgumentException()
}
}
}
이제 EntityService를 테스트 하기위해 service를 spyk 객체로 생성을 해야한다.
updateCount가 실행 될 때, 내부 메소드들(repository의 findByid, save 뿐만 아니라 사용자가 만든 메소드)이
정상적으로 실행 됐는지 검증 할 수있다.
val entityRepository: EntityRepository = mockk()
val entityService: EntityService = spyk(EntityService(mappingRepository))
위 코드와 같이 mocking된 Repository를 주입해 생성하면 된다.
선언할때 Private 메소드를 사용하겠다는 옵션을 넣어주면 된다.
val entityRepository: EntityRepository = mockk()
val entityService: EntityService = spyk(
EntityService(mappingRepository),
recordPrivateCalls = true
)
메소드 사용은 아래와 같다.
validate() 호출시 항상 통과하게 만드는 코드다.
val id = 2
every { entityService["validate"](id) } returns Unit
사용된 인자를 Capturing하여 검증하고 싶을땐 Slot과 mutableList를 사용 할 수 있다.
val mock = mockk<Some>()
val slotId = slot<Long>()
every { mock.someMethod(capture(slotId)) } returns 0
mock.someMethod(5L)
val realId = slotId.captured
assertEquals(5L, realId)
You can capture an argument to a CapturingSlot or MutableList
val car = mockk<Car>()
val slot = slot<Double>()
val list = mutableListOf<Double>()
every {
car.recordTelemetry(
//makes mock match calls with any value for `speed` and record it in a slot
speed = capture(slot),
//makes mock and capturing only match calls with specific `direction`.
//Use`any()` to match calls with any `direction`
direction = Direction.NORTH
)
} answers {
println(slot.captured)
Outcome.RECORDED
}
every {
car.recordTelemetry(
speed = capture(list),
direction = Direction.SOUTH
)
} answers {
println(list)
Outcome.RECORDED
}
car.recordTelemetry(speed = 15, direction = Direction.NORTH) // prints 15
car.recordTelemetry(speed = 16, direction = Direction.SOUTH) // prints 16
verify(exactly = 2) { car.recordTelemetry(speed = or(15, 16), direction = any()) }
confirmVerified(car)