본문 바로가기
Spring

[Spring] Kotest와 Mockk를 사용할 때 주의할 점

by 노력남자 2023. 12. 26.
반응형

이번 포스팅에선 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()) }
            }
        }
    }
})

 

참고

 

https://kotest.io/docs/framework/integrations/mocking.html

반응형

댓글