Kotest 테스트 작성 시 IsolationMode.InstancePerTest와 clearAllMocks()를 사용하는 이유 본문
Kotest 테스트 작성 시 IsolationMode.InstancePerTest와 clearAllMocks()를 사용하는 이유
00rigin 2025. 4. 28. 13:10코루틴을 사용하는 테스트에서는 더욱 필수적이다.
Kotlin + Kotest로 테스트를 작성할 때,
IsolationMode.InstancePerTest와 clearAllMocks()를 함께 사용하는 경우를 자주 볼 수 있다.
특히 코루틴을 사용하는 테스트라면, 이 설정은 선택이 아니라 필수에 가깝다.
이번 글에서는 이 둘을 왜 사용하는지, 어떤 문제가 방지되는지, 그리고 구체적인 예시까지 정리해본다.
FunSpec이란?
Kotest는 다양한 테스트 스타일을 지원하는데,
그중 FunSpec은 자연어처럼 테스트를 작성할 수 있는 스타일이다.
class ExampleTest : FunSpec({
test("설명 문장처럼 테스트를 작성할 수 있다") {
// 테스트 코드
}
})
테스트 코드 예시
아래 코드는 Kotest로 작성된 ArticleListenerTest 일부이다.
class ArticleListenerTest : FunSpec({
val articleRepository = mockk<ArticleRepository>()
val usersRepository = mockk<UsersRepository>()
val producerService = mockk<ProducerService>()
val testScope = TestScope(StandardTestDispatcher())
val articleListener = ArticleListener(
articleRepository,
usersRepository,
producerService,
testScope
)
afterContainer {
clearAllMocks()
}
isolationMode = IsolationMode.InstancePerTest
test("아티클 생성 이벤트가 주어졌을 때 이벤트 처리 테스트") {
// given, when, then 구조로 테스트 작성
}
})
여기서 주목할 부분은 afterContainer { clearAllMocks() }와 isolationMode = InstancePerTest 설정이다.
이 둘이 어떤 역할을 하는지 자세히 살펴보자.
IsolationMode.InstancePerTest란?
IsolationMode.InstancePerTest는 테스트 케이스마다 테스트 클래스 인스턴스를 새로 생성하도록 하는 설정이다.
기본 흐름
- 보통 Kotest는 테스트 클래스 인스턴스를 하나만 만들어 여러 테스트를 실행한다.
- 이 경우, 클래스 필드(mock, repository 등)가 모든 테스트 사이에서 공유된다.
문제점
- 하나의 테스트에서 필드를 변경하거나 mock을 설정하면
- 다른 테스트에 의도치 않은 영향을 미칠 수 있다.
해결 방법
IsolationMode.InstancePerTest를 설정하면,
- 매 테스트마다 테스트 클래스 인스턴스를 새로 만들고,
- 필드들도 항상 새 fresh 상태로 시작한다.
즉, 각 테스트는 완전히 독립적으로 실행된다.
왜 특히 코루틴 테스트에서는 IsolationMode가 중요한가?
코루틴을 사용하는 테스트는 비동기적으로 스케줄링되기 때문에,
조금이라도 상태를 공유하면
- 실행 순서에 따라 결과가 달라지거나
- 테스트가 가끔 통과하고 가끔 실패하는 플레이크 테스트(Flaky Test) 가 발생할 수 있다.
코루틴 환경에서는 테스트 간 상태 공유가 훨씬 더 위험하다.
따라서 코루틴을 사용하는 테스트에서는
IsolationMode.InstancePerTest 설정이 필수적이라고 할 수 있다.
요약
"비동기 작업이 있는 테스트는 항상 독립적으로 실행되어야 예측 가능한 결과를 얻을 수 있다."
clearAllMocks()란?
clearAllMocks()는 모든 mock과 spy의 기록과 설정을 초기화하는 함수이다.
하는 일
- every {}로 설정한 동작(initial behavior)
- verify {}로 기록된 호출(invocations) 모두 초기화한다.
왜 필요할까?
테스트가 여러 개 있을 때,
이전 테스트의 mock 설정이 다음 테스트에 남아 있을 경우,
의도하지 않은 테스트 실패가 발생할 수 있다.
clearAllMocks()를 통해 항상 깨끗한 상태로 테스트를 시작할 수 있다.
같이 쓰는 이유
IsolationMode.InstancePerTest | clearAllMocks() | |
무엇을 하나 | 테스트마다 클래스 인스턴스를 새로 생성 | 테스트마다 mock 설정/호출 기록을 초기화 |
방지하는 문제 | 필드 공유로 인한 테스트 간섭 | mock 설정/호출 기록이 남아 생기는 간섭 |
같이 쓰는 이유 | 코루틴 비동기 환경에서도 예측 가능한 테스트를 만들기 위해 |
특히 코루틴을 사용하는 경우,
두 설정을 함께 적용해야 비동기 작업에 의해 발생할 수 있는 테스트 간섭을 확실히 막을 수 있다.
코드 예시로 보는 차이점
clearAllMocks() 없이 테스트할 때 문제
class MyTestWithoutClear : FunSpec({
val service = mockk<MyService>()
test("첫 번째 테스트") {
every { service.getValue() } returns "first"
}
test("두 번째 테스트") {
every { service.getValue() } returns "second" // ❗ 이전 설정이 꼬일 수 있다
}
})
- 두 번째 테스트에서 의도하지 않은 결과가 나올 수 있다.
clearAllMocks()를 사용했을 때
class MyTestWithClear : FunSpec({
val service = mockk<MyService>()
afterContainer {
clearAllMocks()
}
test("첫 번째 테스트") {
every { service.getValue() } returns "first"
}
test("두 번째 테스트") {
every { service.getValue() } returns "second"
}
})
- 테스트가 끝날 때마다 mock 상태가 초기화되어,
- 테스트들이 완벽하게 독립적으로 동작한다.
최종 요약: Kotest + 코루틴 테스트 독립성 확보 방법
Kotest는 기본적으로 코루틴 기반으로 동작한다.
따라서 테스트 내에서 runTest를 사용하거나 비동기 작업을 수행한 후에는
코루틴 실행 컨텍스트나 상태를 초기화할 필요가 있다.
- IsolationMode.InstancePerTest는 코루틴 스코프와 필드들을 초기화하여 테스트 간 상태 공유를 막는다.
- clearAllMocks()는 mock 객체들의 설정 및 호출 기록을 초기화하여, 의도치 않은 테스트 간섭을 막는다.
한 줄 요약:
"Kotest는 코루틴 기반이기 때문에 runTest 이후 상태 초기화를 위해 IsolationMode.InstancePerTest를,
인스턴스별 mockk 설정 초기화를 위해 clearAllMocks()를 사용한다."
참고 링크
'Kotlin' 카테고리의 다른 글
[Kotlin] Scope Function (0) | 2024.12.03 |
---|---|
[Kotlin][JPA] kotlin에서 optional 대신 nullable 사용하기 (0) | 2023.10.09 |
kotlin + Springboot Swagger 적용하기 (0) | 2023.10.01 |
1.1 주생성자 부생성자 (0) | 2023.10.01 |
1. Kotlin + Springboot Entity 생성하기 (0) | 2023.10.01 |