1. 简介
规范测试(Specification Testing)框架是对单元测试框架的重要补充,能更有效地验证应用行为。
本文将介绍 Spek 框架 —— 一个专为 JVM 设计、支持 Java 和 Kotlin 的规范测试框架。它特别适合在 Kotlin 项目中替代或补充 JUnit,提升测试代码的可读性和表达力。
如果你已经厌倦了 testWhenXThenY
这类命名风格,希望测试代码像文档一样清晰,那 Spek 值得一试。
2. 什么是规范测试?
简单来说,规范测试关注的是“系统应该做什么”,而不是“代码怎么实现”。
它强调以业务需求或功能规格为出发点来组织测试,这与 行为驱动开发(BDD) 的理念高度契合。通过贴近自然语言的结构,让开发、测试甚至产品都能理解测试用例的意图。
常见的规范测试框架包括:
这类框架的核心价值在于:✅ 提升可读性 ❌ 不是用来替代单元测试,而是更高层次的抽象。
2.1 什么是 Spek?
Spek 是基于 Kotlin 的 JVM 规范测试框架,设计为 JUnit 5 Test Engine 的插件形式运行。这意味着:
- 可无缝集成到已有 JUnit 5 项目中
- 能与其他测试(如 JUnit 测试类)共存并统一执行
- 完全利用 JUnit Platform 的强大生态(IDE 支持、报告生成等)
⚠️ 注意:虽然也支持 JUnit 4(通过
junit-platform-runner
),但建议新项目直接使用 JUnit 5。
2.2 Maven 依赖配置
要使用 Spek,需添加以下两个核心依赖(版本必须一致):
<dependency>
<groupId>org.jetbrains.spek</groupId>
<artifactId>spek-api</artifactId>
<version>1.1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.spek</groupId>
<artifactId>spek-junit-platform-engine</artifactId>
<version>1.1.5</version>
<scope>test</scope>
</dependency>
其中:
spek-api
:提供测试 DSL 和核心 APIspek-junit-platform-engine
:作为 JUnit 5 引擎执行测试
📌 最新版本请参考 Maven Central
2.3 第一个测试示例
Spek 测试类需继承自 Spek
并传入一个 lambda 构造块:
class FirstSpec : Spek({
// 在这里编写测试逻辑
})
这个结构初看有点怪异,但正是这种 DSL 风格让测试更具可读性。接下来我们看看具体怎么写。
3. 测试风格(Test Styles)
Spek 的核心优势之一是支持多种 DSL 风格,让你用最自然的方式描述测试逻辑。
3.1 given/on/it 风格
这是最推荐的 BDD 风格,结构清晰,语义明确:
关键字 | 含义 |
---|---|
given |
准备测试上下文(前置条件) |
on |
执行操作(触发行为) |
it |
断言结果(期望输出) |
示例:
class CalculatorTest : Spek({
given("A calculator") {
val calculator = Calculator()
on("Adding 3 and 5") {
val result = calculator.add(3, 5)
it("Produces 8") {
assertEquals(8, result)
}
}
}
})
✅ 可读性极强:Given a calculator, on adding 3 and 5, it produces 8.
⚠️ 注意嵌套顺序必须是 given → on → it
,否则逻辑会混乱。
3.2 describe/it 风格
另一种常见风格,更接近 RSpec 或 Jasmine:
class CalculatorTest : Spek({
describe("A calculator") {
val calculator = Calculator()
describe("Addition") {
val result = calculator.add(3, 5)
it("Produces the correct answer") {
assertEquals(8, result)
}
}
}
})
优点:
- 更灵活,
describe
可无限嵌套 - 适合复杂场景的分层描述
缺点:
- 不如
given/on/it
语义明确 - 容易写出意义模糊的
describe("xxx")
📌 建议团队统一风格,避免混用造成维护困难。
3.3 其他可用关键字
Spek 实际上提供了更多语义化关键字供自由组合:
given
on
describe
context
(等价于given
,用于区分不同上下文)
例如:
class AuthTest : Spek({
context("User is logged in") {
given("has admin role") {
on("accessing dashboard") {
it("should be allowed") { /* ... */ }
}
}
}
})
✅ 核心规则不变:所有断言必须放在 it
块内,且不能在顶层直接写 it
。
3.4 数据驱动测试
由于 Spek 的 DSL 本质是 Kotlin 函数调用,因此可以轻松实现数据驱动测试。
常用方式:遍历数据集合,动态生成测试用例。
class DataDrivenTest : Spek({
describe("A data driven test") {
mapOf(
"hello" to "HELLO",
"world" to "WORLD"
).forEach { input, expected ->
describe("Capitalising $input") {
it("Correctly returns $expected") {
assertEquals(expected, input.toUpperCase())
}
}
}
}
})
✅ 这种写法避免了重复代码,同时保持高可读性。
⚠️ 注意:每个 it
块会被视为独立测试,所以 IDE 和报告中会显示多个测试项。
4. 断言(Assertions)
Spek 本身不绑定任何断言库,你可以自由选择最顺手的工具。
推荐选项:
库 | 特点 |
---|---|
org.junit.jupiter.api.Assertions |
默认选择,熟悉度高 |
Kluent | Kotlin 友好,链式语法 |
Expekt | 简洁流畅 |
HamKrest | 匹配器模式,适合复杂断言 |
示例对比
使用 JUnit 原生断言:
assertEquals(8, result)
使用 Kluent 后更像自然语言:
result shouldEqual 8
✅ 强烈建议搭配 Kluent 或类似库,显著提升测试可读性,减少认知负担。
5. 前置与后置处理器(Before/After Handlers)
和大多数测试框架一样,Spek 支持生命周期钩子:
钩子 | 触发时机 |
---|---|
beforeGroup |
进入当前组前执行一次 |
afterGroup |
离开当前组后执行一次 |
beforeEachTest |
每个 it 前执行 |
afterEachTest |
每个 it 后执行 |
这些块可以嵌套在任意层级,作用域遵循“就近原则”。
执行顺序示例
class GroupTest5 : Spek({
describe("Outer group") {
beforeEachTest {
println("BeforeEachTest 0")
}
beforeGroup {
println("BeforeGroup 0")
}
afterEachTest {
println("AfterEachTest 0")
}
afterGroup {
println("AfterGroup 0")
}
describe("Inner group 1") {
beforeEachTest {
println("BeforeEachTest 1")
}
beforeGroup {
println("BeforeGroup 1")
}
afterEachTest {
println("AfterEachTest 1")
}
afterGroup {
println("AfterGroup 1")
}
it("Test 1") {
println("Test 1")
}
}
}
})
输出结果:
BeforeGroup 0
BeforeGroup 1
BeforeEachTest 0
BeforeEachTest 1
Test 1
AfterEachTest 1
AfterEachTest 0
AfterGroup 1
AfterGroup 0
📌 关键规律:
beforeGroup
全部先于beforeEachTest
执行(外层 → 内层)afterEachTest
先于afterGroup
执行(内层 → 外层)- 类似栈结构,先进后出
踩坑提醒:不要在 beforeEachTest
中做耗时操作,会影响所有 it
的执行效率。
6. 测试主体(Test Subjects)
当整个 Spec 只针对单一被测对象时,可以用 SubjectSpek
简化管理。
通过 subject { }
块声明被测实例,在后续测试中用 subject
直接引用。
使用示例
class CalculatorTest : SubjectSpek<Calculator>({
subject { Calculator() }
describe("A calculator") {
describe("Addition") {
val result = subject.add(3, 5)
it("Produces the correct answer") {
assertEquals(8, result)
}
}
}
})
✅ 优势:
- 避免重复创建对象
- 明确表达“这是本次测试的核心对象”
- 提升可读性,尤其在大型测试套件中
6.1 启用 Subject 扩展的 Maven 依赖
需要额外引入扩展包:
<dependency>
<groupId>org.jetbrains.spek</groupId>
<artifactId>spek-subject-extension</artifactId>
<version>1.1.5</version>
<scope>test</scope>
</dependency>
⚠️ 注意版本必须与其他 Spek 依赖保持一致,否则运行时报错。
7. 总结
Spek 是一个强大的 Kotlin 测试框架,具备以下亮点:
✅ 高度可读的 DSL:让测试代码像文档一样清晰
✅ 灵活的测试风格:支持 given/on/it
、describe/it
等多种组织方式
✅ 无缝集成 JUnit 5:可与现有测试共存,无需改造构建流程
✅ 支持数据驱动和生命周期控制:满足复杂测试场景
✅ 配合 Kluent 等库实现极致表达力
📌 适用场景:
- Kotlin 项目为主
- 团队推行 BDD / 测试即文档
- 希望提升测试可维护性和协作效率
虽然学习成本略高于传统 JUnit,但长期来看,其带来的可读性和维护性收益远超投入。值得一试!