Atrium断言核心语法详解:expect、链式调用与expectation-group,新手必须掌握的3个概念
【免费下载链接】atriumA multiplatform expectation library for Kotlin项目地址: https://gitcode.com/gh_mirrors/atr/atrium
Atrium 是一个面向 Kotlin 的开源多平台断言库(官方称之为 expectation library,即"期望库"),支持 JVM、JS 与 Android。这篇文章用最少的代码,讲清楚新手使用 Atrium 断言库必须掌握的 3 个核心语法概念:expect入口函数、链式调用与 expectation-group,并解读它的失败报告格式,帮你在 10 分钟内写出可读、可调试的 Kotlin 断言。
30 秒认识 Atrium 断言库
与其他断言库相比,Atrium 的设计目标很明确:帮你理解"哪里出了问题"。它的几个特点:
| 特性 | 说明 |
|---|---|
| 🌍 多平台 | 同一套 API 覆盖 JVM、JS、Android 等多平台 |
| ✒️ 两种 API 风格 | fluent(点式)与 infix(中缀),可按团队口味选择或混用 |
| 📋 人话报告 | 失败信息像自然语言一样可读,并区分"断言目标"与"期望内容" |
| 🧩 可扩展 | 可自定义断言函数、期望动词,甚至替换核心组件 |
Atrium 没有像很多库那样叫断言(assertion),而是叫期望(expectation)。看一个最简示例(依赖ch.tutteli.atrium:atrium-fluent:1.2.0,发布在 Maven Central):
import ch.tutteli.atrium.api.fluent.en_GB.* import ch.tutteli.atrium.api.verbs.expect val x = 10 expect(x).toEqual(9)当期望不成立时,抛出的AssertionError信息长这样:
I expected subject: 10 (kotlin.Int) ◆ to equal: 9 (kotlin.Int)报告可以直译为:"我期望 subject(实际是 10)应该等于 9"。第一行是断言目标(subject)的实际值,每个◆开头的一行是一个未满足的期望。这种"主语 + 期望列表"的格式,是贯穿 Atrium 所有报告的基本结构,先记住它。
💡 Atrium 提供两种 API 风格,例如
expect(x).toEqual(9)(fluent)与expect(x) toEqual 9(infix)。完整差异对照可查阅仓库中的apis/differences.md。
概念一:expect —— Atrium 断言库的统一入口
一切从expect开始。它的定义非常直接(源码位于misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/expect.kt):
fun <T> expect(subject: T): RootExpect<T>理解expect,只需要 3 点:
- subject 只求值一次:
expect(x)括号里的表达式只计算一次,之后整条断言链都针对这同一个 subject。这对有副作用或开销较大的表达式尤其重要。 - 它返回的
Expect<T>是"断言扩展点":几乎所有断言函数(toEqual、toContain、toThrow……)都是Expect<T>的扩展函数。接口定义在atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/creating/Expect.kt,这也是你以后编写自定义断言函数的落点。 - "expect" 只是默认动词:Atrium 允许你用自己的期望动词(比如
verify)替换默认词,让测试代码读起来更像业务语言。
expect有两种直接形态,对应了本文的另外两个概念:
expect(x).toEqual(9) // 形态 A:单条断言,可以接着链式调用 expect(x) { toEqual(9) } // 形态 B:直接开一个 expectation-group另外,expect还可以接收一个 lambda 作为 subject,配合toThrow断言异常:
expect { throw IllegalArgumentException("name is empty") }.toThrow<IllegalArgumentException>()概念二:链式调用 —— fail-fast 的断言链
Atrium 允许你在同一个 subject 上连续声明多个单条断言,expect(...)只需写一次:
// 两条单条断言,但只有第一条会被评估 expect(4 + 6).toBeLessThan(5).toBeGreaterThan(10)报告只有第一条期望:
I expected subject: 10 (kotlin.Int) ◆ to be less than: 5 (kotlin.Int)⚠️关键点:链式调用是 fail-fast(快速失败)语义——toBeLessThan(5)已经不成立,后面的toBeGreaterThan(10)根本不会被评估,也不会出现在报告里。这和"每个断言独立成句"的直觉不同,是新手最容易误解的地方。
如果觉得长链读起来费劲,可以用and作为填充词提升可读性:
expect(5).toBeGreaterThan(2).and.toBeLessThan(10)链式调用还有一个重要能力:收窄 subject 的类型。例如toBeAnInstanceOf、notToEqualNull这类函数既做检查、又把 subject 变成新类型,后续断言作用在新 subject 上:
expect(slogan) // subject 类型是 String? .notToEqualNull() // 期望非空后,subject 收窄为 String .toStartWith("atrium")💡 如果习惯中缀风格,同样的链可以写成
expect(5) toBeGreaterThan 2 and toBeLessThan 10(infix 风格里and两侧需要特殊写法,细节见apis/differences.md)。
概念三:expectation-group —— 收集所有失败,一次性报告
当你希望多个期望全部被评估、把所有问题一次看全时,就用 expectation-group 语法——给expect追加一个代码块:
// expectation-group 里有两条期望,全部都会被评估 expect(4 + 6) { toBeLessThan(5) toBeGreaterThan(10) }报告会把两条失败都列出来:
I expected subject: 10 (kotlin.Int) ◆ to be less than: 5 (kotlin.Int) ◆ to be greater than: 10 (kotlin.Int)它的规则可以总结为三点:
- 块内所有期望都会执行,块内不再适用 fail-fast;
- 在闭合
}处统一抛出AssertionError,报告包含全部失败; - 可以无限嵌套。Atrium 在很多地方(类型收窄、特性提取、异常断言等)都提供"带代码块"的重载,块内就是 expectation-group 语义。
比 AssertJ 的软断言更顺手
expectation-group 的理念类似 AssertJ 的 soft assertions,但使用体验更轻:
- 不需要
assertSoftly之类的额外工具,同一个 subject 直接用expect即可; - 不需要重复书写 subject,块内直接写期望函数;
- 块语法不只存在于顶层,嵌套在特性提取(
feature/its)、异常断言(toThrow { ... })内部同样可用。
例如对一个人的多个属性同时检查(feature用于从 subject 提取特性):
expect(person) { feature({ f(it::firstName) }) { toStartWith("P") } feature { f(it::lastName) }.toEqual("Stoll") }连接两个组:and 与 expectGrouped
- 两个 expectation-group 之间可以用
and连接,形成"前一组失败则后一组不评估"的短路关系; - 如果你要对多个互不相关的 subject分别声明期望并希望统一报告,可以使用
expectGrouped { ... }(1.1.0 起提供),它是专为"多 subject 分组"设计的入口。
一张表看懂:链式调用 vs expectation-group
| 维度 | 链式调用.a().b() | expectation-group{ a(); b() } |
|---|---|---|
| 执行策略 | fail-fast,第一条失败即停 | 块内全部评估 |
| 抛错时机 | 失败的那一步立即抛出 | 在闭合}处统一抛出 |
| 报告内容 | 只含第一条失败 | 含全部失败,一次看全 |
| 典型场景 | 简单、独立的快速检查 | 想一次性暴露所有问题,或深层嵌套的复合期望 |
两者不是二选一:外层用链式调用组织流程,需要"全量检查"的局部用 expectation-group,是 Atrium 最自然的组合方式。
新手最容易踩的 3 个坑
- 以为链会全部执行:链式调用是 fail-fast,前面的断言失败时,后面的断言既不会执行也不会出现在报告里。想"一次看全"请改用 expectation-group。
- 以为 subject 会被反复求值:
expect(4 + 6)中的4 + 6只求值一次,整条链/整个组共享这同一个 subject。 - 混淆"带块"与"不带块"两种重载:Atrium 的收窄/提取类函数(如
toBeAnInstanceOf、feature)通常有两个重载——带代码块的保持原 subject、块内是组语义;不带代码块的会把 subject 收窄为新的类型,后续链式调用作用在新 subject 上。写错重载不会报错,但断言对象会"悄悄变化"。
✅ 小提示:Atrium 默认报告器只报告失败的期望,所以通过的部分看不到不代表没检查。
项目结构导航:想深入时去哪看源码
| 模块 | 路径 | 作用 |
|---|---|---|
| 核心类型与报告 | atrium-core/src/commonMain/kotlin/ch/tutteli/atrium/ | Expect接口、报告器、错误格式化 |
| 断言实现 | logic/atrium-logic/src/commonMain/kotlin/ch/tutteli/atrium/logic/ | 各类型断言函数的具体逻辑 |
| fluent API | apis/fluent/atrium-api-fluent/src/commonMain/kotlin/ch/tutteli/atrium/api/ | 点式 API 的断言函数(en_GB 包下是英文措辞) |
| infix API | apis/infix/atrium-api-infix/src/commonMain/kotlin/ch/tutteli/atrium/api/ | 中缀 API 的断言函数 |
| 一站式依赖 | bundles/fluent/atrium-fluent/ | 引入atrium-fluent即包含 API + 核心 + 翻译 |
| 期望动词 | misc/atrium-verbs/src/commonMain/kotlin/ch/tutteli/atrium/api.verbs/expect.kt | expect/expectGrouped的定义 |
| API 差异对照 | apis/differences.md | fluent 与 infix 的命名差异清单 |
更多带输出示例的官方用法,可直接阅读仓库根目录的README.md中 Examples 一节。
小结
expect(subject)是入口:subject 只求值一次,返回的Expect<T>是所有断言的扩展点;- 链式调用表达单条断言,fail-fast、失败即停;
- expectation-group用代码块包裹多条期望,全部评估后统一报告,可嵌套、可
and连接; - 报告格式恒为"I expected subject: 实际值 + ◆ 未满足的期望",读懂它就读懂了 Atrium 的错误输出。
掌握这 3 个概念,你就已经可以覆盖日常 Kotlin 测试中 90% 的 Atrium 用法了。
【免费下载链接】atriumA multiplatform expectation library for Kotlin项目地址: https://gitcode.com/gh_mirrors/atr/atrium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考