1. 概述

Kotest 是一个基于 Kotlin 编写的多平台测试框架,核心由三个子项目构成:

  • 测试框架:提供灵活的测试结构与执行模型
  • 断言库:支持流畅风格的断言(fluent assertions)
  • 属性测试(Property Testing):用于生成随机数据验证函数通用行为

这三个模块可以独立使用,也可以与其他测试框架组合。例如,你完全可以在 JUnit Jupiter 中引入 Kotest 的断言库替代 AssertJ。

Kotest 支持 JVM、JavaScript 和 Native 平台,这意味着一套测试代码可覆盖后端服务、移动端(如 Kotlin Multiplatform Mobile)和前端逻辑,非常适合跨平台项目。

⚠️ 本文聚焦于 JVM 平台下的单元测试实践,其他平台配置略过。


2. 在 JVM 上运行测试

Kotest 基于 JUnit Platform 构建,因此在 Maven 或 Gradle 项目中需要引入对应的运行器依赖。

Maven 配置示例:

<dependency>
    <groupId>io.kotest</groupId>
    <artifactId>kotest-runner-junit5-jvm</artifactId>
    <version>5.1.0</version>
    <scope>test</scope>
</dependency>

Gradle 用户则应添加:

testImplementation("io.kotest:kotest-runner-junit5-jvm:5.1.0")

同时别忘了启用 JUnit Platform:

// build.gradle.kts
tasks.withType<Test> {
    useJUnitPlatform()
}

✅ 引入后,所有继承自 Kotest Spec 的类将自动被识别为测试用例。


3. 测试风格(Testing Styles)

Kotest 提供多种 DSL 风格的测试组织方式,开发者可根据团队习惯或项目类型自由选择。以下是几种主流风格及适用场景。

3.1. Behavior Spec —— BDD 行为驱动开发风格

适合描述复杂业务流程,结构清晰,读起来像自然语言。

class CardPaymentTests : BehaviorSpec({
    given("账户余额充足") {
        `when`("发起信用卡支付") {
            then("支付应成功完成") {
                // 执行测试逻辑
                performPayment().shouldBeSuccess()
            }
        }
    }
})

💡 注意:when 是 Kotlin 关键字,所以要用反引号包裹成 `when`

这种写法特别适合需求文档对齐测试用例,也便于非技术人员理解。


3.2. Should Spec —— 简洁明了的 should 断言风格

强调“应该做什么”,语法轻量,适合中小型项目快速上手。

class MoneyTests : ShouldSpec({
    should("将金额正确转换为目标币种") {
        convert(100, "USD", "EUR") shouldBe 90
    }
})

支持通过 context 分组相关测试,提升可维护性:

class PaymentTests : ShouldSpec({
    context("信用卡支付") {
        should("成功完成一笔卡支付") {
            payWithCard(amount = 100).status shouldBe "SUCCESS"
        }
    }
    context("银行转账") {
        should("支持外部账户转账") {
            transferToBank(account = "DE123").shouldNotBeNull()
        }
    }
})

✅ 推荐团队内部统一使用一种命名规范(中文 or 英文),避免混用影响阅读体验。


3.3. Feature Spec —— 类 Cucumber 的端到端测试风格

适用于编写高阶功能测试,模仿 Cucumber 的 feature-scenario 模型。

class HomePageTests : FeatureSpec({
    feature("用户注册") {
        scenario("允许用户通过邮箱注册") {
            registerUser("user@example.com", "password123")
                .statusCode shouldBe 201
        }
    }
    feature("用户登录") {
        scenario("允许凭有效凭证登录") {
            login("user@example.com", "password123").token.shouldExist()
        }
    }
})

⚠️ 踩坑提示:虽然语义清晰,但过度使用会导致测试文件臃肿。建议仅用于集成测试或关键路径验收测试。


3.4. Describe Spec —— JS/Ruby 风格的 describe-it 结构

深受 JavaScript(Jest/Mocha)和 Ruby 开发者喜爱,结构直观。

class PaymentTests : DescribeSpec({
    describe("信用卡支付") {
        it("应当成功发起支付") {
            makePayment(type = CARD).status shouldBe SUCCESS
        }
    }
    describe("银行转账") {
        it("应当支持跨境汇款") {
            sendWireTransfer(country = "US").fee > 0
        }
    }
})

如果你的团队有前端背景成员,这种风格更容易被接受。


4. 断言(Assertions)

Kotest 自带强大的断言库 kotest-assertions-core,无需额外依赖即可使用丰富的 matcher 方法。

常见断言示例:

// 检查对象相等性
result.shouldBe(expected)

// 布尔表达式成立
result.shouldBeTrue()

// 类型检查
value.shouldBeTypeOf<Double>()

// Map 包含指定 key / values
map.shouldContainKey("userId")
map.shouldContainValues("Alice", "Bob")

// 字符串匹配
text.shouldContain("welcome")
text.shouldBeEqualIgnoringCase("HELLO")

// 文件大小验证
file.shouldHaveFileSize(1024)

// 时间顺序判断
dateA.shouldBeBefore(dateB)

此外还有专用模块扩展支持:

  • kotest-assertions-json:JSON 内容比对
  • kotest-assertions-arrow:函数式编程库 Arrow 支持
  • kotest-assertions-klock:时间处理库 Klock 集成

📌 小技巧:.shouldBe() 在失败时会打印详细 diff,尤其适合对比复杂对象。


5. 异常测试

验证异常抛出非常简洁,使用 shouldThrow<T> 即可捕获并断言异常实例。

val exception = shouldThrow<ValidationException> {
   validate(User(email = "", age = -1))
}

// 进一步验证异常信息
exception.message should startWith("Invalid input")

同样支持 shouldNotThrow 场景:

shouldNotThrow<IllegalArgumentException> {
    createUser("valid@example.com")
}

✅ 相比传统 try-catch 写法更声明式,减少样板代码。


6. 生命周期钩子(Lifecycle Hooks)

类似于 JUnit 的 @BeforeEach / @AfterEach,Kotest 提供了 beforeTestafterTest 来管理测试夹具。

class TransactionStatementSpec : ShouldSpec({
    beforeTest {
        // 准备测试数据:插入交易记录
        testDataRepository.insertSampleTransactions()
    }

    afterTest { (description, result) ->
        // 清理资源:删除测试数据
        testDataRepository.clearAll()
    }

    should("生成正确的账单摘要") {
        generateStatement(userId = 123).total shouldBe 500
    }
})

可用钩子汇总:

钩子 触发时机
beforeSpec 整个测试类开始前
afterSpec 整个测试类结束后
beforeContainer 容器块(如 context)执行前
afterContainer 容器块执行后
beforeEach / afterEach 每个测试节点前后

⚠️ 注意:不要在钩子里做耗时操作,否则会影响整体测试速度。


7. 数据驱动测试(Data-Driven Tests)

类似 JUnit5 参数化测试,Kotest 使用 withData 实现一组输入驱动多个断言。

首先定义测试数据类:

data class TaxTestData(
    val income: Long,
    val taxClass: TaxClass,
    val expectedTaxAmount: Long
)

然后编写参数化测试:

class IncomeTaxTests : FunSpec({
    withData(
        TaxTestData(1000, ONE, 300),
        TaxTestData(1000, TWO, 350),
        TaxTestData(1000, THREE, 200)
    ) { (income, taxClass, expectedTaxAmount) ->
        calculateTax(income, taxClass) shouldBe expectedTaxAmount
    }
})

📌 必须添加依赖:

<dependency>
    <groupId>io.kotest</groupId>
    <artifactId>kotest-framework-datatest-jvm</artifactId>
    <version>5.1.0</version>
    <scope>test</scope>
</dependency>

✅ 优势:避免重复代码,集中管理测试用例;失败时能定位具体哪条数据出错。


8. 非确定性测试(Non-Deterministic Tests)

对于异步或最终一致性场景(如消息队列消费、缓存更新),传统 sleep + retry 容易写出脆弱代码。

Kotest 提供 eventually 函数优雅解决这类问题:

class TransactionTests : ShouldSpec({
    val transactionRepo = TransactionRepo()

    should("最终完成交易状态更新") {
        eventually({
            duration = 5_000.milliseconds  // 最长等待 5 秒
            interval = FixedInterval(1_000) // 每隔 1 秒重试
        }) {
            transactionRepo.getStatus(120) shouldBe "COMPLETE"
        }
    }
})

工作机制:

  • 在规定时间内不断执行闭包内的断言
  • 一旦通过立即返回,不再继续轮询
  • 超时后抛出最后一次的 assertion error

⚠️ 踩坑提醒:避免滥用 eventually,它会让 CI 构建变慢。优先考虑重构代码使其可同步测试。


9. Mocking(模拟对象)

Kotest 不内置 Mock 框架,推荐搭配 MockK 使用(专为 Kotlin 设计,支持协程、sealed class 等特性)。

示例:

class ExchangeServiceTest : FunSpec({
    val exchangeRateProvider = mockk<ExchangeRateProvider>()
    val service = ExchangeService(exchangeRateProvider)

    test("根据汇率服务返回的汇率进行货币兑换") {
        every { exchangeRateProvider.rate("USDEUR") } returns 0.9

        val result = service.exchange(Money(1200, "USD"), "EUR")
        result shouldBe 1080
    }
})

✅ 建议配合 mockkStaticcoEvery 等高级功能处理静态方法和挂起函数。


10. 测试覆盖率统计

结合 JaCoCo 可以生成测试覆盖率报告,帮助评估测试完整性。

build.gradle.kts 中配置:

tasks.test {
    finalizedBy(tasks.jacocoTestReport)
}

tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        html.required.set(true)
        xml.required.set(true) // 用于 CI 集成
    }
}

生成报告路径:

$buildDir/reports/jacoco/test/html/index.html

📌 建议设置最低覆盖率阈值,并在 CI 流水线中拦截不达标 PR。


11. 使用标签分组测试(Tags)

有时我们希望按环境或性能特征筛选测试,比如跳过耗时较长的集成测试。

可通过 @Tag 注解标记特定测试类:

@Tags(SlowTest)
class SlowIntegrationTests : ShouldSpec({ /* ... */ })

定义标签类:

object SlowTest : Tag()

运行时过滤:

# 只运行非慢速测试
./gradlew test -Dkotest.tags="!SlowTest"

# 只运行慢速测试
./gradlew test -Dkotest.tags="SlowTest"

✅ 实际应用中常用于分离:

  • @Tag(UnitTest)
  • @Tag(IntegrationTest)
  • @Tag(ExternalDependency)(依赖第三方服务)

12. 总结

本文系统介绍了 Kotest 的核心能力:

  • 多种 DSL 风格适配不同团队偏好
  • 流畅断言 + 丰富 matcher 提升可读性
  • 对异步、参数化、异常测试提供原生支持
  • 易于集成 MockK、JaCoCo 等生态工具
  • 支持标签化运行策略,优化 CI 效率

作为现代 Kotlin 项目的首选测试框架之一,Kotest 在表达力和功能性之间取得了良好平衡。

所有示例代码已托管至 GitHub:https://github.com/baeldung/kotlin-tutorials/tree/master/kotlin-kotest/kotest


原始标题:Introduction to Kotest