Kotlin Help

Power-assert compiler plugin

The Kotlin Power-assert compiler plugin improves the debugging experience by providing detailed failure messages with contextual information. It simplifies the process of writing tests by automatically generating intermediate values in failure messages. It helps you understand why a test failed without needing complex assertion libraries.

This is an example message provided by the plugin:

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

The Power-assert plugin key features:

  • Enhanced error messages: The plugin captures and displays the values of variables and sub-expressions within the assertion to clearly identify the cause of failure.

  • Simplified testing: Automatically generates informative failure messages, reducing the need for complex assertion libraries.

  • Support for multiple functions: By default, it transforms assert() function calls but can also transform other functions, such as require(), check(), and assertTrue().

Apply the plugin

To enable the Power-assert plugin, configure your build.gradle(.kts) file as follows:

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

Configure the plugin

The Power-assert plugin provides several options to customize its behavior:

  • functions: A list of fully-qualified function paths. The Power-assert plugin will transform the calls to these functions. If not specified, only kotlin.assert() calls will be transformed by default.

  • includedSourceSets: A list of Gradle source sets that the Power-assert plugin will transform. If not specified, all test source sets will be transformed by default.

To customize the behavior, add the powerAssert {} block to you build script file:

// build.gradle.kts powerAssert { functions = listOf("kotlin.assert", "kotlin.test.assertTrue", "kotlin.test.assertEquals", "kotlin.test.assertNull") includedSourceSets = listOf("commonMain", "jvmMain", "jsMain", "nativeMain") }
// build.gradle powerAssert { functions = ["kotlin.assert", "kotlin.test.assertTrue", "kotlin.test.assertEquals", "kotlin.test.assertNull"] includedSourceSets = ["commonMain", "jvmMain", "jsMain", "nativeMain"] }

Since the plugin is Experimental, you will see warnings every time you build your app. To exclude these warnings, add this @OptIn annotation before declaring the powerAssert {} block:

import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi @OptIn(ExperimentalKotlinGradlePluginApi::class) powerAssert { ... }

Use the plugin

This section provides examples of using the Power-assert compiler plugin.

See the complete code of the build script file build.gradle.kts for all these examples:

import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { kotlin("jvm") version "2.1.0" kotlin("plugin.power-assert") version "2.1.0" } group = "org.example" version = "1.0-SNAPSHOT" repositories { mavenCentral() } dependencies { testImplementation(kotlin("test")) } tasks.test { useJUnitPlatform() } @OptIn(ExperimentalKotlinGradlePluginApi::class) powerAssert { functions = listOf("kotlin.assert", "kotlin.test.assertEquals", "kotlin.test.assertTrue", "kotlin.test.assertNull", "kotlin.require", "org.example.AssertScope.assert") }

Assert function

Consider the following test with the assert() function:

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" } } }

If you run the testFunction() test with the Power-assert plugin enabled, you get the explicit failure message:

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

To get a more complete error message, always inline the variable into the test function parameters. Consider the following test function:

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) } }

The output of the executed code doesn't provide enough information to find the cause of the problem:

Assertion failed assert(isValidName && isValidAge) | | | false true

Inline the variable into the assert() function:

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) } }

After execution, you get more explicit information about what went wrong:

Assertion failed assert(person.name.startsWith("A") && person.name.length > 3 && person.age > 20 && person.age < 29) | | | | | | | | | | | | | | | | | | | false | | | | | | | | 10 | | | | | | | Person(name=Alice, age=10) | | | | | | true | | | | | 5 | | | | Alice | | | Person(name=Alice, age=10) | | true | Alice Person(name=Alice, age=10)

Beyond assert function

The Power-assert plugin can transform various functions beyond assert which is transformed by default. Functions like require(), check(), assertTrue(), assertEqual() and others can also be transformed, if they have a form that allows taking a String or () -> String value as the last parameter.

Before using a new function in a test, specify the function in the powerAssert {} block of your build script file. For example, the require() function:

// build.gradle.kts import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi @OptIn(ExperimentalKotlinGradlePluginApi::class) powerAssert { functions = listOf("kotlin.assert", "kotlin.require") }

After adding the function, you can use it in your tests:

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

The output for this example uses the Power-assert plugin to provide detailed information about the failed test:

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

The message shows the intermediate values that lead to the failure, making it easier to debug.

Soft assertions

The Power-assert plugin supports soft assertions, which do not immediately fail the test but instead collect assertion failures and report them at the end of the test run. This can be useful when you want to see all assertion failures in a single run without stopping at the first failure.

To enable soft assertions, implement the way you will collect error messages:

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") } } }

Add these functions to the powerAssert {} block to make them available for the Power-assert plugin:

@OptIn(ExperimentalKotlinGradlePluginApi::class) powerAssert { functions = listOf("kotlin.assert", "kotlin.test.assert", "org.example.AssertScope.assert") }

After that, you could use it in your test code:

// Import the assertSoftly() function import org.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}" } } } } }

In the output, all the assert() function error messages will be printed one after another:

Charlie has an invalid salary: 40000 assert(employee.salary > 50000) { "${employee.name} has an invalid salary: ${employee.salary}" } | | | | | false | 40000 Employee(name=Charlie, age=55, salary=40000) Dave has an invalid age: 150 assert(employee.age < 100) { "${employee.name} has an invalid age: ${employee.age}" } | | | | | false | 150 Employee(name=Dave, age=150, salary=70000)

What's next

Last modified: 25 September 2024