본문 바로가기

Kotest 테스트 작성 시 IsolationMode.InstancePerTest와 clearAllMocks()를 사용하는 이유 본문

Kotlin

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()를 사용한다."

참고 링크

Comments