이번 포스팅에선 Kotest와 Mockk를 같이 사용할 때 주의할 점에 대해 알아보겠다.
아래 테스트를 돌리면 어떻게 될까? 성공할까? 실패할까?
Kotest의 Isolation Mode를 공부했다면 당연히 답할 수 있는 문제다.
class UserServiceTest : DescribeSpec({
describe("getUser") {
val userService = mockk<UserService>()
context("유저가 있는 경우") {
every { userService.getUser(any()) } returns User(1, "노력남자")
it("User를 리턴한다.") {
userService.getUser(1) shouldBe User(1, "노력남자")
verify(exactly = 1) { userService.getUser(any()) }
}
}
context("유저가 없는 경우") {
every { userService.getUser(any()) } returns null
it("null을 리턴한다.") {
userService.getUser(1) shouldBe null
verify(exactly = 1) { userService.getUser(any()) }
}
}
}
})
아래와 같이 실패한다.
이유가 뭘까?
Kotest의 기본 Isolation Mode는 SingleInstance라 목 객체를 전체 테스트에서 공유하기 때문이다.
첫 번째 테스트에 사용된 userService와 두 번째 테스트에 사용된 userService가 같은 목 객체라는 뜻이다.
그래서 첫 번째 verify 때는 이상이 없었는데, 스터빙이 남아있는 userService를 또 호출했기 때문에 두 번째 verify에서 검증이 실패된 것이다.
해결책
해결 방법은 간단하다. 테스트 간 목 객체의 스터빙을 공유하지 않게 만들면 된다.
1. mock 객체를 테스트 마다 생성
context 단위로 목 객체를 새로 생성해주는 방식이다.
context는 Container기 때문에 beforeContainer를 사용해줘야 한다.
class UserServiceTest : DescribeSpec({
describe("getUser") {
lateinit var userService: UserService // lateinit var로 변경
beforeContainer { // 추가
userService = mockk<UserService>()
}
context("유저가 있는 경우") {
every { userService.getUser(any()) } returns User(1, "노력남자")
it("User를 리턴한다.") {
userService.getUser(1) shouldBe User(1, "노력남자")
verify(exactly = 1) { userService.getUser(any()) }
}
}
context("유저가 없는 경우") {
every { userService.getUser(any()) } returns null
it("null을 리턴한다.") {
userService.getUser(1) shouldBe null
verify(exactly = 1) { userService.getUser(any()) }
}
}
}
})
2. 테스트가 끝날 때마다 목 객체의 스터빙을 초기화
같은 목 객체를 사용하되 각 테스트가 끝나면 clearMocks로 스터빙을 초기화 해주는 방법이다.
afterContainer 위치는 맨 위에 위치해도 상관없지만 after니까 맨 아래에 위치시켜주는 게 이해하기 좋다.
*혹시라도 clearMocks를 매번 쓰기 귀찮다고 clearAllMocks를 사용하면 큰일난다. 해당 테스트에 있는 목 객체뿐만 아니라 같이 돌리고 있는 테스트의 목 객체도 초기화하기 때문이다.
class UserServiceTest : DescribeSpec({
describe("getUser") {
val userService = mockk<UserService>()
context("유저가 있는 경우") {
every { userService.getUser(any()) } returns User(1, "노력남자")
it("User를 리턴한다.") {
userService.getUser(1) shouldBe User(1, "노력남자")
verify(exactly = 1) { userService.getUser(any()) }
}
}
context("유저가 없는 경우") {
every { userService.getUser(any()) } returns null
it("null을 리턴한다.") {
userService.getUser(1) shouldBe null
verify(exactly = 1) { userService.getUser(any()) }
}
}
afterContainer { // 추가
clearMocks(userService)
}
}
})
3. Isolation Mode를 InstancePerLeaf로 변경하기 (추천)
it 기준으로 테스트가 동작하게 Isolation Mode를 InstancePerLeaf로 바꿔주는 방법이다.
1, 2번은 테스트 작성할 때마다 clearMocks를 넣어줘야 하는 불편함이 있어서 별로인 거 같다. 누락할 수도 있고.
갑자기 Isolation Mode를 바꾸는 게 쉽지 않겠지만 이 방법이 제일 적용하기 쉬운 거 같다.
class UserServiceTest : DescribeSpec({
isolationMode = IsolationMode.InstancePerLeaf // 추가
describe("getUser") {
val userService = mockk<UserService>()
context("유저가 있는 경우") {
every { userService.getUser(any()) } returns User(1, "노력남자")
it("User를 리턴한다.") {
userService.getUser(1) shouldBe User(1, "노력남자")
verify(exactly = 1) { userService.getUser(any()) }
}
}
context("유저가 없는 경우") {
every { userService.getUser(any()) } returns null
it("null을 리턴한다.") {
userService.getUser(1) shouldBe null
verify(exactly = 1) { userService.getUser(any()) }
}
}
}
})
참고
'Spring' 카테고리의 다른 글
[Spring] Kotest 병렬 테스트 설정 방법 (23) | 2023.12.31 |
---|---|
[Spring] Gradle Test events were not received 해결 방법 (21) | 2023.12.28 |
[Spring] Kotest DescribeSpec, BehaviorSpec에서 Isolation Mode에 따른 동작 방법 알아보기 (2) | 2023.12.25 |
[Spring] Kotest context, when 단위로 @Transactional 동작하게 하는 방법 (18) | 2023.12.11 |
[Spring] Kotest에서 @Transactional을 사용하는 방법 (3) | 2023.12.10 |
댓글