用Kotlin测试Java与Spring应用:低风险的渐进引入路径
考虑采用或已承诺采用 Kotlin 的团队,常常把迁移规划为一系列重写:一次将一个微服务从 Java 替换为 Kotlin 对应实现。实践中,这种计划往往在产出任何已交付代码之前就停滞了。Kotlin 在 JVM 生态中的角色正在扩大,因为它并不要求这种程度的承诺。Netflix、Adobe 和 DoorDash 等公司都是增量式采用 Kotlin,与现有 JVM 系统并存,而不是进行全量重写。风险最低的起点是测试。测试能带来 Kotlin 的语言优势,包括空安全、数据类和富有表现力的 lambda,同时无需触碰任何生产环境运行的东西。
本指南将介绍如何仅针对测试源,将 Kotlin 添加到现有的 Java + Spring Boot 项目中。你将配置 Gradle、用 Kotlin 编写 JUnit 5 测试、使用 MockK 进行 Kotlin 原生模拟,并应用 Kotest 断言来提升测试可读性。你的生产服务不会改变,团队将在一个有边界、无风险的上下文中开始体验 Kotlin。
为什么测试是引入 Kotlin 最安全的地方
Kotlin 测试源与生产应用制品相互隔离,并且不需要在生产运行时中引入 Kotlin。它们仍可能影响 CI 和测试基础设施,但不会触及生产服务。这种与生产环境的分离,使得测试成为团队尚未承诺采用的语言的合适切入点。
在测试源中引入 Kotlin 意味着你现有的 Java 服务完全保持不变:无需重构、没有迁移风险,也不会在生产路径上引入回归。它还允许团队在本来就要编写的测试中试验真实的 Kotlin 语法,而不是在人为构造的示例上试验。当工程师编写的是他们真正愿意写的测试时,开发者的接受度会更快建立。
Kotlin 测试通常更短、更易读,在代码审查中也更容易快速浏览。领导和平台团队有充分理由对语言变更加以怀疑。“我们想用 Kotlin 写测试”这一提案在评估时几乎不带来风险,因为它不会触及生产环境或部署基础设施。这是一个干净、有边界的实验,而且该模式已被行业内的工程组织验证。
[LOADING...]
将 Kotlin 添加到现有的 Java + Spring Boot 项目
你不必为了在测试中使用 Kotlin 而迁移整个构建。如果你运行的是标准的 Spring Boot + Gradle 项目,只需添加 Kotlin JVM 插件,在测试作用域引入 kotlin("stdlib") 和 io.mockk:mockk,IntelliJ IDEA 就会自动将 src/test/kotlin 识别为源集。
在 IntelliJ IDEA 中打开项目并更新 build.gradle.kts,在 plugins 块中添加 Kotlin JVM 插件,并添加测试作用域依赖:
始终要在新项目中固定版本之前,查看 Kotlin 和相关库文档以获取最新稳定版本。如果你使用 Maven,则需要添加 kotlin-maven-plugin:
保存后,IntelliJ IDEA 会显示通知,提示重新加载 Gradle 项目。在 macOS 上使用 Shift+Cmd+I,在 Windows 和 Linux 上使用 Ctrl+Shift+O 触发同步。同步完成后,IntelliJ IDEA 会自动将 src/test/kotlin 识别为源集,让你可以在现有 Java 测试基础设施(如基类、testcontainer 设置、测试数据工厂等)旁使用 Kotlin。
在 Kotlin 中编写 JUnit 6 测试
用 Kotlin 编写 JUnit 6 测试可以消除大量测试设置中的摩擦。该语言的表现力让你能够专注于场景和预期行为。
考虑一个管理订单的 Spring 服务。测试代码往往花更多时间构造测试数据,而不是测试行为。在 Java 中,创建同一对象的多个变体可能需要反复调用 setter:
随着测试套件引入更多场景,设置会变得更加重复。例如,针对大额订单的测试仅为了更改总额,就不得不重复同样的样板代码。Kotlin 的命名参数和默认参数已经可以减少这种重复,但你还可以更进一步,创建一个小型测试数据 DSL,根据测试关心的场景来描述订单:
现在,测试可以将所需数据描述为场景:
该 DSL 使用 Kotlin 的带接收者 lambda,使 OrderScenario 的属性和函数可以直接在 orderScenario { ... } 块中使用。默认值提供合理的测试数据,而各个测试只覆盖与其场景相关的属性。最重要的是,生产环境中的 Order 类可以完全保持 Java 不变。
随着测试套件增长,这种方法尤其有用。测试不再需要累积 cancelledOrder()、largePendingOrder() 或 confirmedOrderForCustomer() 这类专用工厂方法,DSL 为测试提供了一套小型词汇,让测试自行组合这些场景。测试描述的是它需要什么状态,而不重复构造 Java 对象的机制。
一个完整的 JUnit 6 测试随后可以结合场景 DSL、MockK 和 Kotest 断言:
用反引号分隔的测试名称(returns order when found by id)让你可以直接在函数声明中编写描述性测试名,因此该名称会自然地出现在测试运行器输出中。当你想要可读的测试名称时,这也能让你不必使用 JUnit 的 @Named 注解。当 Java 仓库方法返回可空的 Order? 时,Kotlin 编译器会强制测试代码在使用结果之前处理空值情况。这样可以在编译时暴露测试与 Java API 契约之间的不匹配,而不是在测试执行期间以意外的 NullPointerException 形式出现。完整指南见文档。
使用 MockK 进行符合 Kotlin 习惯的模拟
在向 Java 应用添加 Kotlin 测试时,Mockito 仍然是可行选择,但 MockK 提供了围绕 Kotlin 语法设计的 API。其简洁的 every { ... } returns ... 和 verify { ... } 结构让模拟交互更易读,同时允许 Kotlin 测试在不修改生产代码的情况下模拟现有 Java 类。在测试使用 Reactor 的 Java Spring 应用时,这尤其有用。生产应用可以完全保持 Java,并继续返回 Mono 或 Flux。Kotlin 仅引入到测试套件中。MockK 负责处理被模拟的依赖,而 Reactor 的 StepVerifier 负责验证生成的发布者。
考虑一个以响应式方式返回订单的 Java 服务:
可以在不修改其实现的情况下从 Kotlin 测试该服务:
生产服务和仓库可以保持 Java。Kotlin 测试使用 MockK 对 Kotlin 友好的语法来定义模拟行为,而 StepVerifier 检查返回的 Mono 是否发出预期值并成功完成。同样的模式也适用于返回 Flux 的 API。例如,测试可以验证多个发出的值,而无需手动订阅发布者:
MockK 并不会替代 Reactor 的测试工具。每个库负责测试中的不同部分。MockK 提供简洁的模拟,而 StepVerifier 提供针对 Reactor 发布者的断言。Kotlin 将这些 API 汇聚到一个紧凑的测试中,聚焦于被验证的行为。在实践中,这意味着你可以只在测试源集中采用 Kotlin,使用 MockK 模拟现有 Java 依赖,并测试响应式 Java 代码,而无需将 Kotlin 或协程引入生产应用。
用于更清晰测试意图的 Kotest 断言
你不需要 Kotest 的完整测试运行器,也能从其断言库中受益。kotest-assertions-core 依赖添加了中缀匹配器(shouldBe、shouldNotBe、shouldThrow、shouldContain),它们叠加在标准 JUnit 5 测试之上。比较 JUnit 的断言风格与 Kotest:
Kotest 版本的阅读顺序与描述预期的句子一致。这种顺序让意图一目了然,而无需解析 assertEquals 的参数顺序。shouldThrow 对测试 Spring 服务的异常路径尤其有用:
该匹配器会返回抛出的异常,因此你可以在其属性上继续链式断言,而无需额外的 try/catch 块。
使用 @MockkBean 的 Spring Boot 集成测试
对于 Spring 切片测试(@WebMvcTest、@DataJpaTest 或部分 @SpringBootTest),你可以用 springmockk 库中的 @MockkBean 替换 Spring 的 @MockBean。它与 @MockBean 一样集成到 Spring 的测试上下文中,但会创建 MockK 实例:
@MockkBean 注解在测试期间替换 Spring 上下文中的服务。IntelliJ IDEA 可以解析 MockMvc 的 Kotlin DSL(mockMvc.get(...))并提供完整的自动补全支持,从而消除查找链式方法名带来的摩擦。
[LOADING...]
在测试中开始使用 Kotlin
在任何生产代码变更之前,通过测试源引入 Kotlin,是一种经过验证的增量策略。工具链已经成熟,Gradle 配置只需几行,第一个 Kotlin 测试可以在你配置依赖的当天就交付。
随着时间推移,编写 Kotlin 测试可能会改变团队对这门语言的看法。花几个月编写 Kotlin 测试的开发者会开始问:为什么不能在生产环境中使用数据类?他们会怀念 Java 服务代码中的空安全。渐进式采用会自然发生,由真实的偏好驱动,而不是自上而下的强制要求。
JetBrains 与 Spring 团队建立了官方战略合作,专注于改善后端工程师的开发者体验。如果你想探索 Kotlin 在测试套件之外还能为团队做什么,kotlinlang.org 是合适的起点。