12.2.6 Power-assert 编译器插件
原文链接: https://kotlinlang.org/docs/power-assert.html
12.2.6 Power-assert 编译器插件
实验性
Kotlin Power-assert 编译器插件通过提供带有上下文信息的详细失败消息来改善调试体验。它通过在失败消息中自动生成中间值,简化了编写测试的过程。它帮助你理解测试为何失败,而无需复杂的断言库。
下面是该插件提供的一条示例消息:
1
2
3
4
5
6
| Incorrect length
assert(hello.length == world.substring(1, 4).length) { "Incorrect length" }
| | | | | |
| 5 | | "orl" 3
"Hello" | "world!"
false
|
Power-assert 插件的主要特性:
- 增强的错误消息:该插件会捕获并显示断言中变量和子表达式的值,从而清楚地指出失败原因。
- 运行时库:该库提供
@PowerAssert 注解和 CallExplanation 类。它们直接与编译器插件的转换集成,使支持 Power-assert 的函数更容易被发现和配置。 - 简化的测试:自动生成信息丰富的失败消息,减少对复杂断言库的需求。
- 支持多个函数:默认情况下它会转换
assert() 函数调用,但也可以转换其他函数,例如 require()、check() 和 assertTrue()。
应用插件
Gradle
要启用 Power-assert 插件,请按如下方式配置你的 build.gradle(.kts) 文件:
Kotlin
1
2
3
4
5
| // build.gradle.kts
plugins {
kotlin("multiplatform") version "2.4.20"
kotlin("plugin.power-assert") version "2.4.20"
}
|
Groovy
1
2
3
4
5
| // 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() 调用。compilationFilter 控制 Power-assert 插件应用于哪些 Kotlin 编译。你可以创建自己的自定义过滤器,或使用预定义选项:PowerAssertCompilationFilter.TESTS 应用于所有测试源集(默认)。PowerAssertCompilationFilter.ALL 应用于所有源集。
注意: compilationFilter 选项取代了已弃用的 includedSourceSets,后者列出 Power-assert 插件要转换的 Gradle 源集。这两个选项互斥:如果指定了 includedSourceSets,compilationFilter 会被忽略。
要自定义行为,请在构建脚本文件中添加 powerAssert {} 块:
Kotlin
1
2
3
4
5
6
7
| // 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")
}
}
|
Groovy
1
2
3
4
5
6
7
| // build.gradle
powerAssert {
functions = ["kotlin.assert", "kotlin.test.assertTrue", "kotlin.test.assertEquals", "kotlin.test.assertNull"]
compilationFilter = PowerAssertCompilationFilter {
it.name in ["commonMain", "jvmMain", "jsMain", "nativeMain"]
}
}
|
由于该插件是实验性的,你每次构建应用时都会看到警告。要去除这些警告,请在声明 powerAssert {} 块之前添加这个 @OptIn 注解:
1
2
3
4
5
6
| import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
@OptIn(ExperimentalKotlinGradlePluginApi::class)
powerAssert {
...
}
|
Maven
要在 Maven 项目中启用 Power-assert 编译器插件,请更新 pom.xml 文件中 kotlin-maven-plugin 的 <plugin> 部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| <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>
<compilerPlugins>
<plugin>power-assert</plugin>
</compilerPlugins>
</configuration>
<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> 部分指定该选项:
1
2
3
4
5
6
7
8
| <configuration>
<pluginOptions>
<option>power-assert:function=kotlin.assert</option>
<option>power-assert:function=kotlin.test.assertTrue</option>
<option>power-assert:function=kotlin.test.AssertEquals</option>
</pluginOptions>
</configuration>
|
使用 Power-assert 插件
本节提供使用 Power-assert 编译器插件的示例。
关于这些示例的完整内容,请查看构建脚本文件 build.gradle.kts 或 pom.xml:
Gradle (Kotlin)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // build.gradle.kts
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
kotlin("multiplatform") version "2.4.20"
kotlin("plugin.power-assert") version "2.4.20"
}
group = "com.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", "com.example.AssertScope.assert")
}
|
Gradle (Groovy)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // build.gradle
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '2.4.20'
id 'org.jetbrains.kotlin.plugin.power-assert' version '2.4.20'
}
group = 'com.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.jetbrains.kotlin:kotlin-test'
}
test {
useJUnitPlatform()
}
powerAssert {
functions = [
'kotlin.assert',
'kotlin.test.assertEquals',
'kotlin.test.assertTrue',
'kotlin.test.assertNull',
'kotlin.require',
'com.example.AssertScope.assert'
]
}
|
Maven
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>maven-power-assert-plugin-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.code.style>official</kotlin.code.style>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
</properties>
<repositories>
<repository>
<id>mavenCentral</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
</repositories>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>2.4.20</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<compilerPlugins>
<plugin>power-assert</plugin>
</compilerPlugins>
<pluginOptions>
<option>power-assert:function=kotlin.assert</option>
<option>power-assert:function=kotlin.require</option>
<option>power-assert:function=kotlin.test.assertTrue</option>
<option>power-assert:function=kotlin.test.assertEquals</option>
<option>power-assert:function=kotlin.test.assertNull</option>
<option>power-assert:function=com.example.AssertScope.assert</option>
</pluginOptions>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-power-assert</artifactId>
<version>2.4.20</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<configuration>
<mainClass>MainKt</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<version>2.4.20</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>2.4.20</version>
</dependency>
</dependencies>
</project>
|
带 @PowerAssert 注解的函数
如果某个函数带有 @PowerAssert 注解,Power-assert 插件会自动转换对它的调用。你无需在构建配置中注册该函数。
你可以在自己声明断言函数时添加 @PowerAssert 注解,也可以使用支持 Power-assert 的库并提供带注解的函数。
要获得详细的失败消息,请在项目中启用 Power-assert 插件的情况下调用该函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import kotlin.test.Test
data class Mascot(val name: String)
class SampleTest {
@Test
fun testAnnotatedFunction() {
val subject: Any? = Mascot(name = "Unknown")
// 如果 assertThat() 在库中带有 @PowerAssert 注解,
// 该插件会自动转换这次调用
assertThat(subject) {
require(subject is Mascot)
check(subject.name == "Kodee")
}
}
}
|
该插件会提供带有中间表达式值的详细失败消息:
1
2
3
4
5
| check(subject.name == "Kodee")
| | |
| | false
| "Unknown"
Mascot(name=Unknown)
|
assert 函数
考虑以下使用 assert() 函数的测试:
1
2
3
4
5
6
7
8
9
10
11
| 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() 测试,你会得到明确的失败消息:
1
2
3
4
5
6
| Incorrect length
assert(hello.length == world.substring(1, 4).length) { "Incorrect length" }
| | | | | |
| 5 | | "orl" 3
"Hello" | "world!"
false
|
要获得更完整的错误消息,请始终把变量内联到测试函数参数中。考虑以下测试函数:
1
2
3
4
5
6
7
8
9
10
11
12
| 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)
}
}
|
执行代码的输出没有提供足够的信息来找出问题原因:
1
2
3
| assert(isValidName && isValidAge)
| |
true false
|
把变量内联到 assert() 函数中:
1
2
3
4
5
6
7
8
9
10
| 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)
}
}
|
执行之后,你会得到关于出错原因的更明确信息:
1
2
3
4
5
| assert(person.name.startsWith("A") && person.name.length > 3 && person.age > 20 && person.age < 29)
| | | | | | | | | |
| | true | | 5 true | 10 false
| "Alice" | "Alice" Person(name=Alice, age=10)
Person(name=Alice, age=10) Person(name=Alice, age=10)
|
assert 之外的函数
Power-assert 插件可以转换 assert 之外的多种函数(assert 默认会被转换)。像 require()、check()、assertTrue()、assertEqual() 等函数也可以被转换,只要它们的最后一个参数允许接受 String 或 () -> String 值。
在测试中使用新函数之前,请把该函数添加到构建文件中。例如 require() 函数:
Gradle (Kotlin)
1
2
3
4
5
6
7
| // build.gradle.kts
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
@OptIn(ExperimentalKotlinGradlePluginApi::class)
powerAssert {
functions = listOf("kotlin.assert", "kotlin.require")
}
|
Gradle (Groovy)
1
2
3
4
5
6
| powerAssert {
functions = [
'kotlin.assert',
'kotlin.require'
]
}
|
Maven
1
2
3
4
5
6
7
|
<configuration>
<pluginOptions>
<option>power-assert:function=kotlin.assert</option>
<option>power-assert:function=kotlin.require</option>
</pluginOptions>
</configuration>
|
添加该函数后,你就可以在测试中使用它:
1
2
3
4
5
6
7
8
| class RequireExampleTest {
@Test
fun testRequireFunction() {
val value = ""
require(value.isNotEmpty()) { "Value should not be empty" }
}
}
|
这个示例的输出使用 Power-assert 插件提供了关于失败测试的详细信息:
1
2
3
4
| Value should not be empty
require(value.isNotEmpty()) { "Value should not be empty" }
| |
"" false
|
消息展示了导致失败的中间值,使调试更容易。
软断言
Power-assert 插件支持软断言:它不会立即让测试失败,而是收集断言失败并在测试运行结束时统一报告。当你想在一次运行中看到所有断言失败、而不是在第一个失败处停止时,这会很有用。
要启用软断言,请实现收集错误消息的方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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 插件可以使用它们:
Gradle (Kotlin)
1
2
3
4
5
6
7
| // build.gradle.kts
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
@OptIn(ExperimentalKotlinGradlePluginApi::class)
powerAssert {
functions = listOf("kotlin.assert", "kotlin.test.assert", "com.example.AssertScope.assert")
}
|
Gradle (Groovy)
1
2
3
4
5
6
7
| powerAssert {
functions = [
'kotlin.assert',
'kotlin.test.assert',
'com.example.AssertScope.assert'
]
}
|
Maven
1
2
3
4
5
6
7
8
|
<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>
|
提示: 你应指定声明 AssertScope.assert() 函数所在的包的完整名称。
之后,你就可以在测试代码中使用它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 导入 assertSoftly() 函数
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() 函数的错误消息会依次打印出来:
1
2
3
4
5
6
7
8
9
10
11
| Charlie has an invalid salary: 40000
assert(employee.salary > 50000) { "${employee.name} has an invalid salary: ${employee.salary}" }
| | |
| 40000 false
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}" }
| | |
| 150 false
Employee(name=Dave, age=150, salary=70000)
|
为你的库添加 Power-assert 支持
如果你是库作者,可以使用 Power-assert 运行时库中的 @PowerAssert 注解和 CallExplanation 类,为你的库开箱即用地添加 Power-assert 支持。
@PowerAssert 注解
@PowerAssert 注解把某个函数标记为支持 Power-assert。如果你的库使用者在其项目中引入了 Power-assert 编译器插件,并调用了你带注解的函数,这些调用会被自动转换,无需额外的构建配置。
要为你的库添加 Power-assert 支持:
- 在你的构建文件中应用 Power-assert 插件。
- 对于 Maven,添加 Power-assert 运行时库作为依赖:
1
2
3
4
5
6
7
8
|
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-power-assert-runtime</artifactId>
<version>2.4.20</version>
</dependency>
</dependencies>
|
对于 Gradle,该依赖会随 Power-assert 编译器插件自动添加。
- 用
@PowerAssert 注解你的断言函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| import kotlin.powerassert.PowerAssert
import kotlin.powerassert.toDefaultMessage
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
@PowerAssert
fun powerAssert(condition: Boolean, @PowerAssert.Ignore message: String? = null) {
contract { returns() implies condition }
if (!condition) {
val explanation = PowerAssert.explanation
?: fail(message)
val equalityErrors = buildList {
for (expression in explanation.expressions) {
if (expression is EqualityExpression && expression.value == false) {
add(expression)
}
}
}
val failureMessage = buildString {
if (message?.isNotBlank() == true) appendLine(message)
append(explanation.toDefaultMessage())
}
fail(failureMessage, equalityErrors)
}
}
|
PowerAssert.explanation 属性提供对包含调用点信息的 CallExplanation 对象的访问。toDefaultMessage() 函数渲染标准的 Power-assert 失败消息。message 参数上的 @PowerAssert.Ignore 注解会把它排除在失败消息之外。
编译器插件会检测 @PowerAssert 注解,并在编译期转换对该函数的调用。
提示: 完整示例请参阅 kotlin-test-power-assert 项目。
CallExplanation 类
CallExplanation 类提供关于调用点的详细信息,包括中间表达式值。这使断言失败消息可以动态渲染,并能更好地与外部工具集成。
当你的库中某个函数带有 @PowerAssert 注解且应用了编译器插件时,转换会在每个调用点自动进行。PowerAssert.explanation 属性在函数体内提供对 CallExplanation 对象的访问。
注意: 如果带注解的函数是从 Java 调用、从未应用 Power-assert 插件的项目调用,或通过反射调用,PowerAssert.explanation 属性可能返回 null。
下面是一个示例,展示如何在带 @PowerAssert 注解的函数内部使用 CallExplanation 来提取源码信息并构建自定义失败消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| 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 提取失败条件的源码并把它包含在失败消息中。
提示: 完整示例请参阅 fluent-assert 项目。
接下来做什么
看看我们的示例项目: