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-jsatrium-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 测试代码的表达力和可维护性。

核心亮点:

  • ✅ 流畅语法 + 类型安全
  • ✅ 结构化失败输出,降低排查成本
  • ✅ 支持分组、描述、属性提取等高级模式
  • ✅ 易于扩展,支持领域驱动断言

虽然本文只覆盖了基础功能,但已足够展现其价值。建议在实际项目中尝试引入,逐步构建更健壮的测试体系。


原始标题:A Guide to Atrium: Assertion Library for Kotlin