Kotlin Multiplatform 프로젝트에서 Lincheck 사용하기

Kotlin Multiplatform 프로젝트에서 Lincheck 사용하기

Lincheck는 Kotlin Multiplatform과 함께 사용하여 JVM을 대상으로 하는 코드를 테스트할 수 있어요. 이 문서에서는 Kotlin Multiplatform 프로젝트에서 Lincheck 테스트를 설정하는 방법을 안내해 드릴게요.

다음 내용을 배우게 됩니다:

  • JVM을 사용하는 타깃을 위해 프로젝트에서 Lincheck 테스트를 실행하도록 설정합니다.
  • 모든 타깃이 공유하는 카운터 데이터 구조 클래스를 만듭니다.
  • 모든 타깃이 공유하는 카운터 테스트와 JVM 전용 테스트를 만듭니다.
  • 테스트를 실행합니다.

출처: Lincheck in Kotlin Multiplatform projects

본문

프로젝트 설정하기

  1. IntelliJ IDEA에서 새 Kotlin Multiplatform 프로젝트를 만듭니다. 프로젝트를 만들 때 다음을 확인해 주세요:

    • Include tests 체크박스가 선택되어 있어야 합니다.
    • 프로젝트에 JVM 기반(서버, 데스크톱, 또는 Android) 타깃이 있어야 합니다. shared/build.gradle.kts 또는 core/build.gradle.kts 파일을 확인하여 코드가 JVM용으로 컴파일되는지 검증할 수 있어요:
    kotlin { 
        // ...
    
        jvm()
    
        // ...
    
        // If you have an Android target, the use of JVM is
        // configured in the `androidLibrary` section
        androidLibrary {
            // ...
    
            compilerOptions {
                jvmTarget = JvmTarget.JVM_11
            }
    
        // ...
    
        }
    }
    

    Server/Desktop + Android 및 Server/Desktop + Android + iOS 프로젝트의 경우: Kotlin은 현재 JVM과 Android 타깃 간에 코드 공유를 지원하지 않습니다. 플랫폼 간 공유 테스트를 만들 수는 없지만, 플랫폼별 테스트를 사용해 Lincheck로 코드를 테스트할 수는 있어요.

    Lincheck의 일반적인 테스트 설정에 대한 자세한 내용은 Lincheck 시작하기 문서를 읽어 보세요.

  2. 프로젝트의 타깃 플랫폼에 따라 테스트 폴더와 Lincheck 의존성을 설정합니다:

Desktop/Android + iOS

플랫폼별 테스트를 위한 디렉토리는 Kotlin Multiplatform 플러그인이 자동으로 만듭니다. 예를 들어 데스크톱 타깃의 경우 shared/src/jvmTest/kotlin입니다.

플랫폼별 테스트 소스 세트에 Lincheck 의존성을 추가하고, commonTest 소스 세트에 kotlin.test 의존성을 추가해 주세요:

// shared/build.gradle.kts
    kotlin {
        sourceSets {
            commonTest.dependencies {
                implementation(libs.kotlin.test)
            }
            jvmTest {
                implementation("org.jetbrains.lincheck:lincheck:3.7")
            }
        }
    }
// shared/build.gradle.kts
    kotlin {
        sourceSets {
            commonTest.dependencies {
                implementation(libs.kotlin.test)
            }
            getByName("androidHostTest").dependencies {
                implementation("org.jetbrains.lincheck:lincheck:3.7")
            }
        }
    }

Server + iOS, Server + Desktop, Server + Desktop + iOS

  1. core/src/jvmTest/kotlin 디렉토리를 만듭니다. Gradle은 JVM을 사용하는 모든 플랫폼에 대해 이 디렉토리의 테스트를 실행합니다.

    데스크톱 타깃이 있는 프로젝트에는 대신 사용할 수 있는 shared/src/jvmTest/kotlin 디렉토리도 있습니다. 다만 서버와 데스크톱/iOS 타깃 모두에 관련된 코드는 core 디렉토리에 두는 것이 좋습니다. 데스크톱 타깃이 있고 core/src/jvmTest/kotlin 디렉토리를 만든다면 shared/src/jvmTest/kotlin 디렉토리는 삭제해 주세요.

  2. jvmTest 소스 세트에 Lincheck 의존성을 추가하고 commonTest 소스 세트에 kotlin.test 의존성을 추가합니다:

    // core/build.gradle.kts
    kotlin {
        // ...
        sourceSets {
            // ...
            commonTest.dependencies {
                implementation(libs.kotlin.test)
            }
            jvmTest.dependencies {
                implementation("org.jetbrains.lincheck:lincheck:3.7")
            }
        }
    }
    

공유 클래스 만들기

모든 타깃이 공유하는 카운터 데이터 구조를 구현하는 클래스를 만듭니다:

  1. core/src/commonMain 또는 shared/src/commonMain 디렉토리에 UnsafeCounter.kt 파일을 만듭니다:

    class UnsafeCounter {
        private var value: Int = 0
    
        fun inc() = value++
    
        fun get(): Int = value
    }
    
  2. 같은 디렉토리에 SafeCounter.kt 파일을 만듭니다:

    import kotlin.concurrent.atomics.AtomicInt
    import kotlin.concurrent.atomics.ExperimentalAtomicApi
    
    @OptIn(ExperimentalAtomicApi::class)
    class SafeCounter {
        private var value = AtomicInt(0)
    
        fun inc() = value.addAndFetch(1)
    
        fun get(): Int = value.load()
    }
    

공유 테스트 작성하기

모든 타깃에서 실행되는 테스트를 작성합니다:

  1. core/src/commonTest 또는 shared/src/commonTest 디렉토리에 UnsafeCounterTest.kt 파일을 만듭니다:

    import kotlin.test.Test
    import kotlin.test.assertEquals
    
    class UnsafeCounterTest {
        @Test
        fun testIncrement() {
            val counter = UnsafeCounter()
            assertEquals(0, counter.get(), "Initial value should be 0")
            counter.inc()
            assertEquals(1, counter.get(), "Value after one increment should be 1")
        }
    }
    
  2. 같은 디렉토리에 SafeCounterTest.kt 파일을 만듭니다:

    import kotlin.test.Test
    import kotlin.test.assertEquals
    
    class SafeCounterTest {
        @Test
        fun testIncrement() {
            val counter = SafeCounter()
            assertEquals(0, counter.get(), "Initial value should be 0")
            counter.inc()
            assertEquals(1, counter.get(), "Value after one increment should be 1")
        }
    }
    

JVM 전용 테스트 작성하기

JVM을 대상으로 하는 플랫폼에서만 실행되는 테스트를 작성합니다:

  1. JVM 전용 테스트 디렉토리UnsafeCounterConcurrentTest.kt 파일을 만듭니다:

    • Android + iOS – shared/src/androidHostTest/kotlin
    • Desktop + iOS – shared/src/jvmTest/kotlin
    • 서버 타깃이 있는 모든 프로젝트 – core/src/jvmTest/kotlin
    import org.jetbrains.lincheck.datastructures.ModelCheckingOptions
    import org.jetbrains.lincheck.datastructures.Operation
    import kotlin.test.Test
    
    class UnsafeCounterConcurrentTest {
        private val c = UnsafeCounter()
    
        @Operation
        fun inc() = c.inc()
    
        @Operation
        fun get() = c.get()
    
        @Test
        fun modelCheckingTest() {
            ModelCheckingOptions().check(this::class)
        }
    }
    
  2. 같은 디렉토리에 SafeCounterConcurrentTest.kt 파일을 만듭니다:

    import org.jetbrains.lincheck.datastructures.ModelCheckingOptions
    import org.jetbrains.lincheck.datastructures.Operation
    import kotlin.test.Test
    
    class SafeCounterConcurrentTest {
        private val c = SafeCounter()
    
        @Operation
        fun inc() = c.inc()
    
        @Operation
        fun get() = c.get()
    
        @Test
        fun modelCheckingTest() {
            ModelCheckingOptions().check(this::class)
        }
    }
    

테스트 실행하기

이 시점에서 프로젝트에는 카운터 데이터 구조의 공유 구현, 공유 테스트, 그리고 카운터용 JVM 전용 테스트가 있어야 합니다. 프로젝트의 디렉토리 구조는 다음과 같아야 합니다:

컨텍스트 메뉴, 단축키, 또는 Gradle 태스크 실행을 통해 개별 테스트를 실행할 수 있어요. allTests Gradle 태스크를 실행하면 프로젝트의 모든 테스트가 각각에 맞는 테스트 러너로 실행됩니다:

예를 들어 JVM 전용 테스트는 JVM 테스트 러너로 실행됩니다:

JVM 이외의 플랫폼 지원

Lincheck가 다른 플랫폼을 지원할 가능성은 낮습니다.

Lincheck는 바이트코드 조작에 JVM을 크게 의존하기 때문에, 다른 플랫폼에서 모델 체킹을 구현하는 것은 어렵습니다. 게다가 Lincheck는 결정적(deterministic) 샌드박스 환경에서 코드를 테스트하므로 여러 플랫폼을 동시에 모델 체킹하는 것은 가치가 거의 없습니다. 테스트 결과는 플랫폼에 관계없이 동일하기 때문이죠.

더 알아보기