아래는 kotest + mockk 으로 코틀린 테스트 코드를 작성하면서 내가 자주 사용하는 부분들을 정리해봤다.
return 값 없는 메소드(Unit) mocking 하기
mock 클래스에서 응답이 없는 메소드더라고 mocking해주지 않으면 no answer 에러가 난다. 이럴 때에는 그냥 실행한다라는 의미의 mocking이 필요하다.
justRun { testService.runMethod() }
every { testService.runMethod() } just runs
every { testService.runMethod() } returns Unit
위 3가지는 모두 같은 의미이다.
주의) 리턴 값이 있지만 사용하지 않는 메소드의 경우 위와 같이 mocking하면 ClassCastException 이 발생한다.
class MemberService (private val memberRepository: MemeberRepository) {
fun saveMember(member: Member) {
...
memberRepository.save(member)
}
}
// saveMember에 대한 테스트 시, memberRepository.save()의 리턴값을 사용하지 않는다고 아래와 같이 mocking하면 Exception 발생
justRun { memberRepository.save(member) }
// java.lang.ClassCastException: class kotlin.Unit cannot be cast to class Member
메소드 실행 횟수 검증
verify를 통해서 호출 횟수를 테스트할 수 있다.
verify(exactly = $호출횟수) { memberRepository.save(member) }
아래는 호출하지 않음을 테스트하는 예제이다
import io.mockk.Called
import io.mockk.verify
class MemberServiceTest : DescribeSpec({
describe("saveMember 메소드는") {
context("사용하지 않는 member의 경우") {
it("저장하지 않는다") {
val member = Member.created().apply { use = false }
memberService.saveMember(member)
// 호출하지 않음을 테스트 1
verify { memberRepository.save(member) wasNot Called }
// 호출하지 않음을 테스트 2
verify(exactly = 0) { memberRepository.save(member) }
}
}
}
})
주의) 같은 메소드에 대한 테스트 케이스가 여러개일 경우 mock clear를 해주지 않으면 앞 테스트 케이스들에서의 호출 횟수가 쌓여서 정상적인 호출 횟수 비교가 안된다. 아래처럼 clearAllMocks()를 afterEach 혹은 beforeEach로 실행해줘야 한다.
afterEach { clearAllMocks() }
메소드 실행 중 사용된 인자를 캡쳐하여 검증하기 - slot
메소드 실행 중에 사용된 인자를 캡쳐해서 그 때의 값을 테스트할 수 있다. 예를 들어 아래 saveMember의 메소드에서 repository에 save하기 전에 받아온 member에 대한 변경이 있었다고 하면, 제대로 변경돼서 repository.save의 인자로 넘어갔는지를 테스트 해야한다.
class MemberService (private val memberRepository: MemeberRepository) {
fun saveMember(member: Member) {
// 인자로 받아온 member를 변경하는 로직
memberRepository.save(member)
}
}
이 경우 Slot객체를 만들고, mocking 시 capture로 넘겨준다. 이후 Slot 객체의 captured로 mocking한 메소드가 실행될 때 넘겼던 인자의 값들을 확인할 수 있다.
class MemberServiceTest : DescribeSpec({
describe("saveMember 메소드는") {
context("~할 경우") {
it("member의 상태 값을 sleeper로 변경해서 저장해야한다.") {
val memberSlot = slot<Member>()
every { memberRepository.save(capture(memberSlot)) } returns member
memberService.saveMember(member) sholudBe member
// saveMember 메소드 내부에서 memberRepository.save에 넘긴 member가 capture된다.
memberSlot.captured.status sholudBe MemberStatus.SLEEPER
}
}
}
})
예외 테스트
직접 try catch로 캐치한 exception의 내부 필드를 비교할 수도 있지만 아래처럼 shouldThrow, shouldThrowExactly, shouldThrowAny를 활용하면 좀 더 kotest 스럽게(?) 테스트가 가능하다.
활용 예시
shouldThrowExactly<ResponseStatusException> {
memberService.saveMember(member)
}.should { e ->
e.status shouldBe HttpStatus.BAD_REQUEST
e.message shouldBe "잘못된 요청입니다."
}
- shouldThrowExactly: throw 되는 Exception의 타입까지 검증. FileNotFoundException의 부모클래스인 IOException를 던지는 것도 허용하지 않는다.
val exception = shouldThrowExactly<FileNotFoundException> {
// test here
}
- shouldThrow: shouldThrowExactly와 마찬가지로 타입까지 검증하나,부모클래스도 허용
val exception = shouldThrowExactly<FileNotFoundException> {
// test here
}
- shouldThrowAny: 예외 타입에 관계없이, 예외를 던지는 여부만을 테스트할 때
val exception = shouldThrowAny { // test here can throw any type of Throwable! }
spky()
mock 객체는 mockk()를 이용해서 선언하는데, 만약 spyk()를 사용한다면 일부만 mocking해서 테스트할 수 있다.
클래스의 일부 메소드만을 mocking 하고, 이외는 그대로 실행까지 테스트하고 싶을 때 아래 memberService같이 mockk 대신 spyk로 생성해주면 된다.
val memberRepository = mockk<MemberRepository>()
val memberService = spyk<MemberService>(memberRepository)
참고
'kotlin' 카테고리의 다른 글
[Kotlin] parameter vs property, Constructor parameter is never used as a property (0) | 2023.01.27 |
---|---|
[Kotlin] Sequence를 이용한 컬렉션 지연 계산, 항상 효율적일까? (0) | 2022.05.10 |
[Kotlin] 문자열 공백 제거 방법 (trim, trimIndent, trimMargin) (0) | 2022.05.03 |
Kotest로 깔끔하게 Kotlin 테스트 코드 작성하기 (0) | 2022.03.31 |
[kotlin vs java ] 코틀린과 자바의 차이, 코틀린의 장점 (1) | 2022.03.31 |