1. 简介
本文将介绍 Atrium,一个专为 Kotlin 设计的现代断言库。我们将了解它的核心特性、适用场景以及如何在项目中集成和使用。
Atrium 是一个支持 JVM、JavaScript 和 Android 平台的类型安全、可扩展且高度流畅的断言库。它提供了两种风格的 API:流式 API(fluent API) 和 中缀 API(infix API),本文主要聚焦于前者。其设计目标是让测试代码更易读、失败信息更清晰,减少调试成本。
✅ 优势总结:
- 类型安全,编译期检查
- 流畅语法,接近自然语言
- 失败输出结构化、可读性强
- 支持自定义断言,易于扩展
2. 依赖配置
使用 Atrium 前,需引入对应依赖。截至本文撰写时,最新版本为 1.2.0。
Maven 配置
<dependency>
<groupId>ch.tutteli.atrium</groupId>
<artifactId>atrium-fluent-jvm</artifactId>
<version>1.2.0</version>
</dependency>
⚠️ 注意:
- 若想使用中缀风格 API,请替换为
atrium-infix-jvm
- 若目标平台是 JavaScript,则使用
atrium-fluent-js
或atrium-infix-js
Gradle 配置
implementation("ch.tutteli.atrium:atrium-fluent:1.2.0")
Gradle 的依赖名称更简洁,无需显式指定平台,构建系统会自动解析正确的变体。
导入完成后即可在测试中使用。
3. 基础断言
使用 Atrium 前,需确保导入以下两个关键包:
import ch.tutteli.atrium.api.fluent.en_GB.*
import ch.tutteli.atrium.api.verbs.expect
expect
是入口函数,用于创建断言主体en_GB
包含了所有英文风格的流式断言方法
基本用法示例
expect(value).toEqual("Hello")
这等价于 JUnit 的 assertEquals
,但失败输出更直观:
I expected subject: "Hello" <1047460013>
◆ to equal: "Hello, World!" <6444850>
其中:
subject
是传给expect()
的实际值to equal
是期望值
其他常见断言包括:
expect(value).toMatch(regex)
expect(value).toBeGreaterThan(min)
✅ 提示:这些断言是类型安全的。例如,非字符串类型不会出现 toMatch()
方法,IDE 会直接提示不可用。
3.1 多个断言
链式断言(短路执行)
对同一值进行多个判断时,可链式调用。一旦某个断言失败,后续不再执行:
expect(value).toBeGreaterThan(0)
.toBeLessThan(5)
若 value = -3
,输出如下:
I expected subject: -3 (kotlin.Int <261841467>)
◆ to be greater than: 0 (kotlin.Int <1907622760>)
清晰指出哪个条件未满足。
分组断言(全量执行)
若希望所有断言都运行并收集全部失败信息,可用 lambda 将断言分组:
expect(value) {
toStartWith("Hello")
toEndWith("World")
}
此时 lambda 内的接收者是 expect()
的值,支持流畅语法。
失败时输出所有不通过的项(仅显示失败项,通过的被省略):
I expected subject: "Wrong" <1765795529>
◆ to start with: "Hello" <48208774>
◆ to end with: "World" <929383713>
这种模式适合验证对象多个独立属性是否符合预期。
3.2 断言分组(Grouped Expectations)
除了对同一对象做多断言,还可以将不同但相关的断言逻辑分组执行,每组独立运行,互不影响。
使用 expectGrouped {}
定义分组,内部通过 group("描述") {}
创建具体组:
expectGrouped {
group("第一组") {
expect("Hello").toEqual("World")
expect("Hello").toEqual("Again")
}
group("第二组") {
expect("Hello").toEqual("World")
}
}
所有组都会被执行,即使前一组失败。每组内默认执行所有断言(类似上节的 block 模式):
# 第一组:
◆ ▶ I expected subject: "Hello" <1744189907>
◾ to equal: "World" <1809194904>
◆ ▶ I expected subject: "Hello" <1744189907>
◾ to equal: "Again" <1219273867>
# 第二组:
◆ ▶ I expected subject: "Hello" <1744189907>
◾ to equal: "World" <1809194904>
✅ 实战技巧:可在循环中动态生成分组,实现简易的数据驱动测试:
expectGrouped {
for (i in 1..3) {
group("第 $i 组") {
expect(i).toBeLessThan(2)
}
}
}
输出:
my expectations:
# 第 2 组:
◆ ▶ I expected subject: 2 (kotlin.Int <2096578823>)
◾ to be less than: 2 (kotlin.Int <2096578823>)
# 第 3 组:
◆ ▶ I expected subject: 3 (kotlin.Int <1070161412>)
◾ to be less than: 2 (kotlin.Int <2096578823>)
3.3 断言描述(because)
有时需要为断言添加上下文说明,帮助理解测试意图或定位问题。
使用 because()
方法添加描述文本,并在其 lambda 中编写具体断言:
expect(value).because("这是预期的默认值") {
toEqual("Hello, World!")
}
失败时输出包含原因:
I expected subject: "Hello" <423583818>
◆ to equal: "Hello, World!" <1029472813>
ℹ because: 这是预期的默认值
⚠️ 注意:because
内部仍是一个 block,可包含多个断言,共用同一描述。
4. 属性断言
除了整体对象比较,更多时候我们需要验证对象的某些字段或方法返回值。
使用 its()
通过 its()
提取属性并断言:
expect(user)
.its(User::username) { toEqual("Username") }
.its(User::displayName) { toEqual("Display Name") }
.its(User::isEnabled) { toEqual(true) }
链式调用会在首个失败处中断:
I expected subject: User(username=baeldung, displayName=Baeldung) (...)
◆ ▶ its.definedIn(FluentAssertionUnitTest.kt:65): "baeldung" <252480153>
◾ to equal: "Username" <1422238463>
如需全量验证,改用 block 形式:
expect(user) {
its(User::username) { toEqual("Username") }
its(User::displayName) { toEqual("Display Name") }
its(User::isEnabled) { toEqual(true) }
}
但默认输出缺少字段名标识,可读性一般。
使用 feature()
(推荐)
feature()
利用反射生成更清晰的字段路径信息:
expect(user) {
feature({ f(it::username) }) { toEqual("Username") }
feature({ f(it::displayName) }) { toEqual("Display Name") }
feature({ f(it::isEnabled) }) { toEqual(true) }
}
失败输出明确标注字段名:
I expected subject: User(username=baeldung, displayName=Baeldung) (...)
◆ ▶ username: "baeldung" <1042790962>
◾ to equal: "Username" <540325452>
◆ ▶ displayName: "Baeldung" <1976804832>
◾ to equal: "Display Name" <1959910454>
✅ 踩坑提醒:feature()
写法稍复杂,建议封装成常用断言避免重复。
5. 类型断言
有时需要验证对象的实际类型而非值。
基础类型检查
利用 reified 泛型进行类型断言:
expect(animal).toBeAnInstanceOf<Cat>()
失败输出清晰展示类型差异:
I expected subject: Dog@32b260fa (Dog <850551034>)
◆ to be an instance of type: Cat -- Class: Cat
类型内断言
还可进一步在特定类型上下文中进行字段验证:
expect(animal).toBeAnInstanceOf<Dog> {
its(Animal::name).toEqual("Lassie")
its(Dog::breed).toEqual("Pomeranian")
}
lambda 内部的接收者已被智能转换为 Dog
类型,可直接访问子类属性,无需强制转换。
失败示例:
I expected subject: Dog@121314f7 (Dog <303240439>)
◆ ▶ its.definedIn(FluentAssertionUnitTest.kt:118): "Rough collie" <1873091796>
◾ to equal: "Pomeranian" <2095064787>
6. 自定义断言
Atrium 最强大的特性之一就是轻松扩展自定义断言。所有内置断言本质上都是 Expect<T>
上的扩展函数。
方式一:基于 _logic.createAndAppend
定义最基础的自定义断言:
fun Expect<Int>.toBeAMultipleOf(base: Int) =
_logic.createAndAppend("to be a multiple of", base) { it % base == 0 }
使用:
expect(3).toBeAMultipleOf(2) // 失败
输出:
I expected subject: 3 (kotlin.Int <1070161412>)
◆ to be a multiple of: 2 (kotlin.Int <2096578823>)
该断言仅对 Int
类型可见。
方式二:组合现有断言
更常见的做法是复用已有断言构建复合逻辑:
fun Expect<Int>.toBeBetween(low: Int, high: Int) = and {
toBeGreaterThan(low)
toBeLessThan(high)
}
and
表示所有子断言必须通过。失败时沿用原断言的输出格式:
I expected subject: 3 (kotlin.Int <1070161412>)
◆ to be greater than: 5 (kotlin.Int <449308954>)
6.1 领域类型断言
可为业务模型定义专属断言,提升测试可读性。
例如为 User
类型添加:
fun Expect<User>.toHaveUsername(username: String) = its(User::username) { toEqual(username) }
使用方式自然流畅:
expect(user).toHaveUsername("Other")
输出:
I expected subject: User(username=baeldung, displayName=Baeldung) (...)
◆ ▶ its.definedIn(FluentAssertionUnitTest.kt:169): "baeldung" <517254671>
◾ to equal: "Other" <1990519794>
✅ 建议:在大型项目中,集中管理领域断言,形成团队共享的测试 DSL。
7. 总结
Atrium 提供了一套强大而优雅的断言机制,显著提升了 Kotlin 测试代码的表达力和可维护性。
核心亮点:
- ✅ 流畅语法 + 类型安全
- ✅ 结构化失败输出,降低排查成本
- ✅ 支持分组、描述、属性提取等高级模式
- ✅ 易于扩展,支持领域驱动断言
虽然本文只覆盖了基础功能,但已足够展现其价值。建议在实际项目中尝试引入,逐步构建更健壮的测试体系。