Power-assert 컴파일러 플러그인

Power-assert 컴파일러 플러그인

Kotlin Power-assert 컴파일러 플러그인은 컨텍스트 정보가 담긴 자세한 실패 메시지로 디버깅 경험을 개선해 줘요. 실패 메시지에 중간 값을 자동으로 생성해서 테스트 작성 과정을 단순화해요. 복잡한 assertion 라이브러리 없이도 테스트가 왜 실패했는지 이해할 수 있게 도와주죠.

출처: Power-assert compiler plugin

본문

이 플러그인이 제공하는 메시지 예시예요.

Incorrect length
assert(hello.length == world.substring(1, 4).length) { "Incorrect length" }
       |     |      |  |     |               |
       |     5      |  |     "orl"           3
       "Hello"      |  "world!"
                    false

Power-assert 플러그인의 주요 기능은 다음과 같아요.

  • 향상된 오류 메시지 — 플러그인은 assertion 안의 변수와 하위 표현식의 값을 캡처해서 표시해, 실패 원인을 명확히 찾을 수 있게 해 줘요.
  • 런타임 라이브러리 — 라이브러리는 @PowerAssert 애너테이션과 CallExplanation 클래스를 제공해요. 이들은 Power-assert 가능 함수를 더 발견하기 쉽고 구성하기 쉽게 만들어 주는데, 컴파일러 플러그인 변환과 직접 통합되기 때문이에요.
  • 단순화된 테스트 — 정보가 담긴 실패 메시지를 자동으로 생성해 복잡한 assertion 라이브러리의 필요성을 줄여줘요.
  • 여러 함수 지원 — 기본적으로는 assert() 함수 호출을 변환하지만, require(), check(), assertTrue() 같은 다른 함수도 변환할 수 있어요.

플러그인 적용하기

Gradle

Power-assert 플러그인을 활성화하려면 build.gradle(.kts) 파일을 다음과 같이 구성하세요.

// build.gradle.kts
plugins {
    kotlin("multiplatform") version "2.4.20"
    kotlin("plugin.power-assert") version "2.4.20"
}
  
// build.gradle
plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '2.4.20'
    id 'org.jetbrains.kotlin.plugin.power-assert' version '2.4.20'
}

Power-assert 플러그인은 동작을 커스터마이징할 수 있는 몇 가지 옵션을 제공해요.

  • functions — Power-assert 플러그인이 호출 시 변환할 함수의 정규화된 경로를 나열해요. 지정하지 않으면 플러그인은 kotlin.assert() 호출만 변환해요.
  • PowerAssertCompilationFilter.TESTS — 모든 테스트 소스 세트에 적용돼요(기본값).
  • PowerAssertCompilationFilter.ALL — 모든 소스 세트에 적용돼요.

동작을 커스터마이징하려면 빌드 스크립트 파일에 powerAssert {} 블록을 추가하세요.

// build.gradle.kts
powerAssert {
    functions = listOf("kotlin.assert", "kotlin.test.assertTrue", "kotlin.test.assertEquals", "kotlin.test.assertNull")
    compilationFilter = PowerAssertCompilationFilter {
        it.name in setOf("commonMain", "jvmMain", "jsMain", "nativeMain")
    }
}
  
// build.gradle
powerAssert {
    functions = ["kotlin.assert", "kotlin.test.assertTrue", "kotlin.test.assertEquals", "kotlin.test.assertNull"]
    compilationFilter = PowerAssertCompilationFilter {
        it.name in ["commonMain", "jvmMain", "jsMain", "nativeMain"]
    }
}

플러그인이 Experimental이기 때문에 앱을 빌드할 때마다 경고가 보일 거예요. 이 경고를 제외하려면 powerAssert {} 블록을 선언하기 전에 다음 @OptIn 애너테이션을 추가하세요.

Maven

Maven 프로젝트에서 Power-assert 컴파일러 플러그인을 활성화하려면 pom.xml에서 kotlin-maven-plugin<plugin> 섹션을 업데이트하세요.

<build>
    <plugins>
        <plugin>
            <artifactId>kotlin-maven-plugin</artifactId>
            <groupId>org.jetbrains.kotlin</groupId>
            <version>2.4.20</version>
            <executions>
                <execution>
                    <id>compile</id>
                    <phase>process-sources</phase>
                    <goals>
                        <goal>compile</goal>
                    </goals>
                </execution>
                <execution>
                    <id>test-compile</id>
                    <phase>process-test-sources</phase>
                    <goals>
                        <goal>test-compile</goal>
                    </goals>
                </execution>
            </executions>

            <configuration>
                <!-- Specify the Power-assert plugin -->
                <compilerPlugins>
                    <plugin>power-assert</plugin>
                </compilerPlugins>
            </configuration>

            <!-- Add the Power-assert plugin dependency -->
            <dependencies>
                <dependency>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-maven-power-assert</artifactId>
                    <version>2.4.20</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

function 옵션을 사용해 Power-assert 플러그인이 변환할 함수를 커스터마이징할 수 있어요. 예를 들어 kotlin.test.assertTrue(), kotlin.test.assertEquals() 등을 포함할 수 있어요. 지정하지 않으면 기본적으로 kotlin.assert() 호출만 변환돼요.

이 옵션은 kotlin-maven-plugin<configuration> 섹션에서 지정하세요.

Power-assert 플러그인 사용하기

이 절에서는 Power-assert 컴파일러 플러그인을 사용하는 예시를 제공해요.

모든 예시의 빌드 스크립트 파일 build.gradle.kts 또는 pom.xml의 전체 코드를 참고하세요.

@PowerAssert 애너테이션이 붙은 함수

함수에 @PowerAssert 애너테이션이 붙어 있으면, Power-assert 플러그인은 그 함수에 대한 호출을 자동으로 변환해요. 빌드 구성에 함수를 등록할 필요가 없어요.

@PowerAssert 애너테이션은 직접 assertion 함수를 선언할 때 추가할 수 있고, Power-assert를 지원하는 애너테이션이 붙은 함수를 제공하는 라이브러리를 사용할 수도 있어요.

자세한 실패 메시지를 얻으려면 프로젝트에서 Power-assert 플러그인을 활성화한 상태로 함수를 호출하세요.

import kotlin.test.Test

data class Mascot(val name: String)

class SampleTest {

    @Test
    fun testAnnotatedFunction() {
        val subject: Any? = Mascot(name = "Unknown")
        // If assertThat() is annotated with @PowerAssert in the library,
        // the plugin transforms this call automatically
        assertThat(subject) {
            require(subject is Mascot)
            check(subject.name == "Kodee")
        }
    }
}

플러그인은 중간 표현식 값이 담긴 자세한 실패 메시지를 제공해요.

Assert 함수

assert() 함수를 사용하는 다음 테스트를 보세요.

import kotlin.test.Test

class SampleTest {

    @Test
    fun testFunction() {
        val hello = "Hello"
        val world = "world!"
        assert(hello.length == world.substring(1, 4).length) { "Incorrect length" }
    }
}

Power-assert 플러그인을 활성화한 상태로 testFunction() 테스트를 실행하면 명확한 실패 메시지를 얻어요.

Incorrect length
assert(hello.length == world.substring(1, 4).length) { "Incorrect length" }
       |     |      |  |     |               |
       |     5      |  |     "orl"           3
       "Hello"      |  "world!"
                    false

더 완전한 오류 메시지를 얻으려면 항상 변수를 테스트 함수 파라미터에 인라인하세요. 다음 테스트 함수를 보세요.

class ComplexExampleTest {

    data class Person(val name: String, val age: Int)

    @Test
    fun testComplexAssertion() {
        val person = Person("Alice", 10)
        val isValidName = person.name.startsWith("A") && person.name.length > 3
        val isValidAge = person.age in 21..28
        assert(isValidName && isValidAge)
    }
}

실행된 코드의 출력은 문제의 원인을 찾기에 충분한 정보를 주지 않아요.

assert(isValidName && isValidAge)
       |              |
       true           false

변수를 assert() 함수에 인라인하세요.

class ComplexExampleTest {

    data class Person(val name: String, val age: Int)

    @Test
    fun testComplexAssertion() {
        val person = Person("Alice", 10)
        assert(person.name.startsWith("A") && person.name.length > 3 && person.age > 20 && person.age < 29)
    }
}

실행 후에는 무엇이 잘못됐는지 더 명확한 정보를 얻어요.

assert 함수 너머

Power-assert 플러그인은 기본으로 변환되는 assert 외에도 다양한 함수를 변환할 수 있어요. require(), check(), assertTrue(), assertEqual() 같은 함수도, 마지막 파라미터로 String 또는 () -> String 값을 받는 형태를 가지면 변환할 수 있어요.

테스트에서 새 함수를 사용하기 전에 빌드 파일에 그 함수를 추가하세요. 예를 들어 require() 함수의 경우:

// build.gradle.kts
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi

@OptIn(ExperimentalKotlinGradlePluginApi::class)
powerAssert {
    functions = listOf("kotlin.assert", "kotlin.require")
}
  
// build.gradle
powerAssert {
    functions = [
            'kotlin.assert',
            'kotlin.require'
    ]
}
  
<!-- pom.xml -->
<configuration>
    <pluginOptions>
        <option>power-assert:function=kotlin.assert</option>
        <option>power-assert:function=kotlin.require</option>
    </pluginOptions>
</configuration>

함수를 추가한 뒤 테스트에서 사용할 수 있어요.

class RequireExampleTest {

    @Test
    fun testRequireFunction() {
        val value = ""
        require(value.isNotEmpty()) { "Value should not be empty" }
    }
}

이 예시의 출력은 실패한 테스트에 대한 자세한 정보를 제공하기 위해 Power-assert 플러그인을 사용해요.

Value should not be empty
require(value.isNotEmpty()) { "Value should not be empty" }
        |     |
        ""    false

메시지는 실패로 이끄는 중간 값을 보여 줘서 디버깅을 더 쉽게 만들어요.

소프트 assertion(Soft assertions)

Power-assert 플러그인은 소프트 assertion을 지원해요. 소프트 assertion은 테스트를 즉시 실패시키지 않고 assertion 실패를 모아서 테스트 실행이 끝날 때 보고해요. 첫 실패에서 멈추지 않고 한 번의 실행으로 모든 assertion 실패를 보고 싶을 때 유용해요.

소프트 assertion을 활성화하려면 오류 메시지를 수집할 방식을 구현하세요.

fun <R> assertSoftly(block: AssertScope.() -> R): R {
    val scope = AssertScopeImpl()
    val result = scope.block()
    if (scope.errors.isNotEmpty()) {
        throw AssertionError(scope.errors.joinToString("\n"))
    }
    return result
}

interface AssertScope {
    fun assert(assertion: Boolean, message: (() -> String)? = null)
}

class AssertScopeImpl : AssertScope {
    val errors = mutableListOf<String>()
    override fun assert(assertion: Boolean, message: (() -> String)?) {
        if (!assertion) {
            errors.add(message?.invoke() ?: "Assertion failed")
        }
    }
}

Power-assert 플러그인에서 사용할 수 있게 이 함수들을 빌드 파일에 추가하세요.

// build.gradle.kts
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi

@OptIn(ExperimentalKotlinGradlePluginApi::class)
powerAssert {
    functions = listOf("kotlin.assert", "kotlin.test.assert", "com.example.AssertScope.assert")
}
  
// build.gradle
powerAssert {
    functions = [
            'kotlin.assert',
            'kotlin.test.assert',
            'com.example.AssertScope.assert'
    ]
}
  
<!-- pom.xml -->
<configuration>
    <pluginOptions>
        <option>power-assert:function=kotlin.assert</option>
        <option>power-assert:function=kotlin.require</option>
        <option>power-assert:function=com.example.AssertScope.assert</option>
    </pluginOptions>
</configuration>

그런 다음 테스트 코드에서 사용할 수 있어요.

// Import the assertSoftly() function
import com.example.assertSoftly

class SoftAssertExampleTest1 {

    data class Employee(val name: String, val age: Int, val salary: Int)

    @Test
    fun `test employees data`() {
        val employees = listOf(
            Employee("Alice", 30, 60000),
            Employee("Bob", 45, 80000),
            Employee("Charlie", 55, 40000),
            Employee("Dave", 150, 70000)
        )

        assertSoftly {
            for (employee in employees) {
                assert(employee.age < 100) { "${employee.name} has an invalid age: ${employee.age}" }
                assert(employee.salary > 50000) { "${employee.name} has an invalid salary: ${employee.salary}" }
            }
        }
    }
}

출력에서는 모든 assert() 함수 오류 메시지가 차례로 출력돼요.

라이브러리에 Power-assert 지원 추가하기

라이브러리 작성자라면 Power-assert 런타임 라이브러리의 @PowerAssert 애너테이션과 CallExplanation 클래스를 사용해 라이브러리에 바로 사용 가능한 Power-assert 지원을 추가할 수 있어요.

@PowerAssert 애너테이션

@PowerAssert 애너테이션은 함수를 Power-assert 가능 함수로 표시해요. 라이브러리 사용자들의 프로젝트에 Power-assert 컴파일러 플러그인이 있고, 여러분의 애너테이션이 붙은 함수를 호출하면, 추가 빌드 구성 없이 그 호출이 자동으로 변환돼요.

라이브러리에 Power-assert 지원을 추가하려면:

  1. 빌드 파일에서 Power-assert 플러그인을 적용해요.
  2. Maven에서는 런타임 라이브러리를 의존성으로 추가해요.
    <!-- pom.xml -->
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-power-assert-runtime</artifactId>
            <version>2.4.20</version>
        </dependency>
    </dependencies>
    
    Gradle에서는 이 의존성이 Power-assert 컴파일러 플러그인과 함께 자동으로 추가돼요.
  3. @PowerAssert 애너테이션으로 함수를 표시하고, 함수 본문에서 PowerAssert.explanation 프로퍼티를 사용해 CallExplanation 객체에 접근해요.
    • PowerAssert.explanation 프로퍼티는 호출 지점(call site) 정보를 담은 CallExplanation 객체에 접근을 제공해요.
    • toDefaultMessage() 함수는 표준 Power-assert 실패 메시지를 렌더링해요.
    • 메시지 파라미터의 @PowerAssert.Ignore 애너테이션은 그 메시지를 실패 메시지에서 제외해요.

컴파일러 플러그인은 @PowerAssert 애너테이션을 감지해서 컴파일 타임에 함수에 대한 호출을 변환해요.

CallExplanation 클래스

CallExplanation 클래스는 중간 표현식 값을 포함한 호출 지점에 대한 자세한 정보를 제공해요. 이는 assertion 실패에 대한 동적 메시지 렌더링과 외부 도구와의 더 나은 통합을 가능하게 해 줘요.

라이브러리의 함수에 @PowerAssert가 붙어 있고 컴파일러 플러그인이 적용되면, 각 호출 지점에서 변환이 자동으로 수행돼요. PowerAssert.explanation 프로퍼티는 함수 본문 안에서 CallExplanation 객체에 접근을 제공해요.

@PowerAssert 애너테이션이 붙은 함수 안에서 CallExplanation을 사용해 소스 코드 정보를 추출하고 커스텀 실패 메시지를 만드는 예시예요.

package kotlinx.test.fluent

import kotlin.powerassert.PowerAssert
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

@PowerAssert
fun AssertScope<*>.check(condition: Boolean) {
    if (!condition) {
        val explanation = PowerAssert.explanation
        val message = if (explanation == null) null else {
            val conditionArg = explanation.arguments.last()!!
            val source = explanation.source.substring(conditionArg.startOffset, conditionArg.endOffset)
            "Condition failed: $source"
        }
        collect(message, explanation)
    }
}

@OptIn(ExperimentalContracts::class)
@PowerAssert
fun AssertScope<*>.require(condition: Boolean) {
    contract { returns() implies condition }
    if (!condition) {
        val explanation = PowerAssert.explanation
        val message = if (explanation == null) null else {
            val conditionArg = explanation.arguments.last()!!
            val source = explanation.source.substring(conditionArg.startOffset, conditionArg.endOffset)
            "Condition failed: $source"
        }
        fail(message, explanation)
    }
}

이 예시에서 check() 함수는 실패를 모아 나중에 보고하고, require() 함수는 즉시 실패해요. 두 함수 모두 CallExplanation을 사용해 실패한 조건의 소스 코드를 추출해서 실패 메시지에 포함시켜요.

다음 단계

샘플 프로젝트를 살펴보세요.

  • 플러그인이 활성화된 간단한 프로젝트
  • 여러 소스 세트를 가진 더 복잡한 프로젝트
  • 런타임 라이브러리 기능을 실험하기 위한 예시 모음

더 알아보기

  • Kotlin 컴파일러 플러그인 개요
  • kotlin.test 라이브러리 문서