Kotlin Power-assert 编译器插件通过提供带有上下文信息的详细失败消息, 改善调试的体验. 它通过在失败消息中自动生成中间值, 简化测试代码的编写过程. 它帮助你理解测试失败的原因, 而不需要使用复杂的断言库.
Incorrect length
assert(hello.length == world.substring(1, 4).length) { "Incorrect length" }
| | | | | |
| 5 | | "orl" 3
"Hello" | "world!"
false
应用插件 Gradle 要启用 Power-assert 插件, 请按如下方式配置你的 build.gradle(.kts) 文件:
// build.gradle.kts
plugins {
kotlin("multiplatform") version "2.4.0"
kotlin("plugin.power-assert") version "2.4.0"
}
// build.gradle
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '2.4.0'
id 'org.jetbrains.kotlin.plugin.power-assert' version '2.4.0'
}
Power-assert 插件提供了几种选项来定制它的行为:
functions : 一组完全限定的函数路径的列表. Power-assert 插件将会转换对这些函数的调用. 如果没有指定这个选项, 默认只有对 kotlin.assert() 的调用会被转换.
includedSourceSets : 一组 Power-assert 插件将会转换的 Gradle 源代码集的列表. 如果没有指定这个选项, 默认所有的 测试源代码集 会被转换.
要定制 Power-assert 插件的行为, 请向你的构建脚本文件添加 powerAssert {} 代码块:
// 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"]
}
由于这个插件是 实验性功能 , 你会在每次构建你的应用程序时看到警告. 要排除这些警告, 请在声明 powerAssert {} 代码块之前添加 @OptIn 注解:
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
@OptIn(ExperimentalKotlinGradlePluginApi::class)
powerAssert {
...
}
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.0</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>
<!-- 指定 Power-assert 插件 -->
<compilerPlugins>
<plugin>power-assert</plugin>
</compilerPlugins>
</configuration>
<!-- 添加 Power-assert 插件的依赖项 -->
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-power-assert</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
你可以使用 function 选项, 自定义 Power-assert 插件转换哪些函数. 例如, 你可以包含 kotlin.test.assertTrue(), kotlin.test.assertEquals(), 等等. 如果没有指定, 默认只有对 kotlin.assert() 的调用会被转换.
在 kotlin-maven-plugin 的 <configuration> 部分指定这个选项:
<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 的完整代码:
// build.gradle.kts
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
kotlin("multiplatform") version "2.4.0"
kotlin("plugin.power-assert") version "2.4.0"
}
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")
}
// build.gradle
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '2.4.0'
id 'org.jetbrains.kotlin.plugin.power-assert' version '2.4.0'
}
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'
]
}
<!-- pom.xml -->
<?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.0</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.0</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.0</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.0</version>
</dependency>
</dependencies>
</project>
使用 @PowerAssert 注解的函数 如果一个函数标注了 @PowerAssert 注解, Power-assert 插件会自动转换对它的调用. 你不需要在构建配置中注册这个函数.
你可以在自己声明断言函数时, 添加 @PowerAssert 注解, 或者使用 支持 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")
// 如果库中的 assertThat() 标注了 @PowerAssert 注解,
// 插件会自动转换这个调用
assertThat(subject) {
require(subject is Mascot)
check(subject.name == "Kodee")
}
}
}
插件提供详细的失败消息, 包含中间表达式值:
check(subject.name == "Kodee")
| | |
| | false
| "Unknown"
Mascot(name=Unknown)
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(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, 但也能够转换各种其他函数. 例如 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")
}
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
这段消息显示导致失败的中间值, 使得调试更加容易.
软断言(Soft Assertion) Power-assert 插件支持软断言(Soft 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")
}
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>
然后, 你可以在你的测试代码中使用它:
// 导入 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() 函数错误消息将会逐个打印输出:
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 运行时库添加为依赖项:
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-power-assert-runtime</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
对于 Gradle, 这个依赖项会随 Power-assert 编译器插件自动添加.
使用 @PowerAssert 注解, 标注你的断言函数:
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 注解, 并在编译期间转换对这个函数的调用.
CallExplanation 类CallExplanation 类提供了关于调用点的详细信息, 包括中间表达式值. 这使得断言失败时能够动态输出消息, 并与外部工具更好的集成.
当你的库中的函数标注了 @PowerAssert 注解, 并且编译器插件已应用时, 转换会在每个调用点自动执行. PowerAssert.explanation 属性可以在函数体内访问 CallExplanation 对象.
如果已注解的函数从 Java 调用, 从未应用 Power-assert 插件的项目调用, 或通过 反射 调用, PowerAssert.explanation 属性可能返回 null.
下面的示例, 演示如何在 @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 提取失败条件的源代码, 并将它包含在失败消息中.