测试 Kotlin 多平台应用:共享测试与平台专用测试
原文:Test your multiplatform app − tutorial。作者:JetBrains Kotlin 文档贡献者(原页无个人署名)。原页更新日期为 2026 年 10 月 1 日,本文依据该页内容翻译整理。
本教程介绍如何在 Kotlin Multiplatform 应用中创建、配置和运行测试。多平台测试分两类:共享代码测试可通过支持的框架在目标平台运行;平台专用测试验证各平台独有逻辑,并可使用平台框架更丰富的 API、断言及扩展能力。多平台项目同时支持两类测试。我们先从简单的共享单元测试开始,再扩展到同时包含共享和平台逻辑的例子。
开始前,应熟悉 Kotlin Multiplatform 项目结构,以及 JUnit 等常见单元测试框架的基础知识。Android 本地主机测试需要合适的 JDK 与 Android 开发配置;iOS 测试需要 Mac 和 Xcode。

测试一个简单的多平台项目
创建项目
- 按 快速入门配置 Kotlin Multiplatform 开发环境。
- 在 IntelliJ IDEA 中选择 File → New → Project。
- 左侧选择 Kotlin Multiplatform。
- Name 填
KMP testing,Project ID 填kmp.project.testing。 - 选择 Android 目标;使用 Mac 时也选择 iOS。选择 Do not share UI。
- 取消 Include tests,再点击 Create。后续将手工创建测试源集。
原文的创建窗口截图用于说明这些选项;本文用明确的步骤及源集结构图替代界面截图,选项名称保留英文便于核对。IDE 和插件版本变化时,界面位置可能不同。
编写共享代码
在 sharedLogic/src/commonMain/kotlin 下创建包 common.example.search,再创建 Grep.kt:
package common.example.search
fun grep(lines: List<String>, pattern: String, action: (String) -> Unit) {
val regex = pattern.toRegex()
lines.filter(regex::containsMatchIn)
.forEach(action)
}
此函数模仿 UNIX grep:接收文本行列表、正则表达式模式和回调函数;每当一行匹配模式,就对该行执行回调。
添加共享测试
共享测试源集应依赖 kotlin.test。检查 sharedLogic/build.gradle.kts 中是否已有以下配置;片段应放在对应的 Kotlin 配置上下文中:
sourceSets {
// 其余配置保持不变
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
这里的 libs.kotlin.test 来自项目版本目录,需与模板生成的别名一致。然后:
- 右键
sharedLogic/src,选择 New → Directory。 - 输入并选择
commonTest/kotlin。commonTest 存放共享测试。 - 在该目录下创建包
common.example.search。 - 创建测试文件
Grep.kt,内容如下。它与生产源集里的同名文件位于不同目录;也可命名为GrepTest.kt,类名仍为 GrepTest。
package common.example.search
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
class GrepTest {
companion object {
val sampleData = listOf(
"123 abc",
"abc 123",
"123 ABC",
"ABC 123"
)
}
@Test
fun shouldFindMatches() {
val results = mutableListOf<String>()
grep(sampleData, "[a-z]+") {
results.add(it)
}
assertEquals(2, results.size)
for (result in results) {
assertContains(result, "abc")
}
}
}
导入的注解和断言与具体平台、框架无关;真正运行测试时,由所选平台的测试框架提供运行器。此用例验证小写字母模式找到两行,并检查每个结果包含 abc。
理解 kotlin.test API
kotlin.test 提供平台无关的注解和断言。Test 等注解会映射到所选框架提供的注解或最接近的等价项。断言通过 Asserter 接口的实现执行。API 有默认实现,但通常使用框架提供的实现。
例如 JVM 支持 JUnit 4、JUnit 5 和 TestNG。Android 中调用 assertEquals 可能转给 JUnit4Asserter 实例的 assertEquals;iOS 则使用默认 Asserter 实现与 Kotlin/Native 测试运行器。
运行测试
可以点击 shouldFindMatches 函数旁的运行图标、使用测试文件的右键菜单,或点击 GrepTest 类旁的运行图标。快捷键为 macOS 的 ⌃⇧F10,其他环境可使用 Ctrl+Shift+F10。选择任一方式后,IDE 会让你选择运行目标。
选择 android 时,教程使用 JUnit 4;选择 iosSimulatorArm64 时,Kotlin 编译器识别测试注解并生成测试二进制,由 Kotlin/Native 自身的运行器执行。原文用 IDE 截图展示上游成功测试的界面形态;它不构成其他项目或环境的测试结果。
处理更复杂的项目
为共享逻辑编写测试
现在引入 CurrentRuntime 类,用于记录运行平台的名称和版本。例如 Android 本地单元测试运行在 JVM 上时,值可能是 OpenJDK 和 17.0。构造器接收名称字符串和可空版本字符串;版本存在时,仅提取开头的数字部分。
- 在
commonMain/kotlin下创建包org.kmp.testing。 - 创建
CurrentRuntime.kt:
package org.kmp.testing
class CurrentRuntime(val name: String, rawVersion: String?) {
companion object {
val versionRegex = Regex("^[0-9]+(\\.[0-9]+)?")
}
val version = parseVersion(rawVersion)
override fun toString() = "$name version $version"
private fun parseVersion(rawVersion: String?): String {
val result = rawVersion?.let {
versionRegex.find(it)
}
return result?.value ?: "unknown"
}
}
正则只提取主版本和可选的一段小版本。例如 1.2.3 会得到 1.2;这里不是完整的语义版本解析器。若字符串不以数字开头,结果为 unknown。
- 在
commonTest/kotlin下创建同名包org.kmp.testing。 - 创建
CurrentRuntimeTest.kt,用平台与框架无关的断言覆盖四种情况:
package org.kmp.testing
import kotlin.test.Test
import kotlin.test.assertEquals
class CurrentRuntimeTest {
@Test
fun shouldDisplayDetails() {
val runtime = CurrentRuntime("MyRuntime", "1.1")
assertEquals("MyRuntime version 1.1", runtime.toString())
}
@Test
fun shouldHandleNullVersion() {
val runtime = CurrentRuntime("MyRuntime", null)
assertEquals("MyRuntime version unknown", runtime.toString())
}
@Test
fun shouldParseNumberFromVersionString() {
val runtime = CurrentRuntime("MyRuntime", "1.2 Alpha Experimental")
assertEquals("MyRuntime version 1.2", runtime.toString())
}
@Test
fun shouldHandleMissingVersion() {
val runtime = CurrentRuntime("MyRuntime", "Alpha Experimental")
assertEquals("MyRuntime version unknown", runtime.toString())
}
}
仍可使用前面介绍的任一种 IDE 方式运行。
添加平台专用测试
为了让示例简短,教程使用 expect/actual 声明机制。更复杂的代码通常更适合通过接口和工厂函数隔离平台差异。
在共享的 CurrentRuntime.kt 中追加:
expect fun determineCurrentRuntime(): CurrentRuntime
每个支持的平台必须提供对应实现,否则构建失败。实现之外,也要提供相应测试。下面分别配置 Android 和 iOS。
Android 的实现和本地测试
- 在
androidMain/kotlin下创建包org.kmp.testing。 - 创建
AndroidRuntime.kt,实现共享声明:
package org.kmp.testing
actual fun determineCurrentRuntime(): CurrentRuntime {
val name = System.getProperty("java.vm.name") ?: "Android"
val version = System.getProperty("java.version")
return CurrentRuntime(name, version)
}
- 右键
sharedLogic/src,选择 New → Directory,输入并选择androidHostTest/kotlin。 - 在其中创建包
org.kmp.testing。 - 创建
AndroidRuntimeTest.kt:
package org.kmp.testing
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
class AndroidRuntimeTest {
@Test
fun shouldDetectAndroid() {
val runtime = determineCurrentRuntime()
assertContains(runtime.name, "OpenJDK")
assertEquals("21.0", runtime.version)
}
}
环境前提:OpenJDK 和 21.0 是原文示例环境。要让这个测试通过,应把断言改成实际使用的虚拟机名称和版本;故意观察一次失败也能帮助理解断言报告。本文将原文 assertEquals(runtime.version, "21.0") 调整为常见的 assertEquals(expected, actual) 顺序,判断结果不变,失败信息更准确。
Android 专用测试运行在本地 JVM 上并不矛盾:这是当前机器上的本地单元测试,区别于设备或模拟器中的插桩测试。可参阅 Android 本地测试文档以及原文链接的 Touchlab 测试资料。通过本地 JVM 测试不能证明真实 Android 设备上的表现。
iOS 的实现和测试
- 在
iosMain/kotlin下创建包org.kmp.testing。 - 创建
IOSRuntime.kt:
package org.kmp.testing
import kotlin.experimental.ExperimentalNativeApi
import kotlin.native.Platform
@OptIn(ExperimentalNativeApi::class)
actual fun determineCurrentRuntime(): CurrentRuntime {
val name = Platform.osFamily.name.lowercase()
return CurrentRuntime(name, null)
}
- 右键
sharedLogic/src,创建iosTest/kotlin。 - 在其中创建包
org.kmp.testing。 - 创建
IOSRuntimeTest.kt:
package org.kmp.testing
import kotlin.test.Test
import kotlin.test.assertEquals
class IOSRuntimeTest {
@Test
fun shouldDetectOS() {
val runtime = determineCurrentRuntime()
assertEquals("ios", runtime.name)
assertEquals("unknown", runtime.version)
}
}
实现使用带 ExperimentalNativeApi 标记的原生接口,需显式 opt-in。测试期望系统族名称为 ios,因为版本传入 null,所以版本字符串为 unknown。这里同样把 assertEquals 调整为期望值在前。iOS 目标的编译与运行需要具备对应工具链的 Mac。
运行多个测试并查看报告
此时项目中应有共享代码、平台代码及各自的测试。用目录树明确对应关系:
sharedLogic/
├── build.gradle.kts
└── src/
├── commonMain/kotlin/
│ ├── common/example/search/Grep.kt
│ └── org/kmp/testing/CurrentRuntime.kt
├── commonTest/kotlin/
│ ├── common/example/search/Grep.kt
│ └── org/kmp/testing/CurrentRuntimeTest.kt
├── androidMain/kotlin/org/kmp/testing/AndroidRuntime.kt
├── androidHostTest/kotlin/org/kmp/testing/AndroidRuntimeTest.kt
├── iosMain/kotlin/org/kmp/testing/IOSRuntime.kt
└── iosTest/kotlin/org/kmp/testing/IOSRuntimeTest.kt
除了右键菜单和快捷键,也可以执行 Gradle 测试任务。例如原文的 allTests 会用对应运行器执行项目的测试。测试运行后,IDE 会显示结果,并生成 HTML 报告;原文示例位于 sharedLogic/build/reports/tests。
原文建议运行 allTests 后查看:
allTests/index.html:共享和 iOS 测试的汇总报告。共享测试会纳入相应原生目标测试执行流程。testDebugUnitTest与testReleaseUnitTest:Android 默认 debug/release 变体的报告;原文指出 Android 报告尚未自动并入 allTests 汇总。
版本核对:原文同时使用较新的 androidHostTest 源集名称和历史 Android 任务/报告示例。Android KMP 插件、AGP 及模板变化可能改变任务名、报告目录和聚合关系。以实际 Gradle 任务列表与任务输出为准,不应为了匹配截图而臆造任务。共享测试不等于独立于平台执行一次,通常会编译进各目标的测试任务;实际依赖顺序以当前项目配置为准。
原文的 IDE 成功截图、Gradle 任务截图和 HTML 报告截图用于演示步骤与结果形态。本文用操作步骤、完整源集目录树、测试代码及原创源集图表达可复现信息;测试任务名、路径和报告类别以实际 Gradle/KMP 插件版本为准。截图是上游示例,不代表当前项目环境已通过测试。
多平台测试的使用规则
- 共享代码的测试只使用 kotlin.test 等多平台库,并把依赖放在 commonTest 源集。
- 间接使用 Asserter 即可;虽然 API 能访问它,测试通常不应直接调用具体 Asserter 实例。
- 尽量保持在测试库的公共 API 内。编译器和 IDE 会帮助限制共享代码中使用特定框架能力。
- commonTest 可由不同框架执行;仍应在打算支持的各框架与目标上运行,确认开发环境和适配都正确。
- 考虑平台和设备的物理交互差异。例如滚动惯性和摩擦不同,同样的滚动速度可能产生不同位置,应在目标平台测试组件的实际行为。
- 平台专用测试可以使用相应框架的注解、扩展和其他专有功能。
- 测试既可从 IDE 运行,也可用 Gradle 任务运行;执行后通常自动生成 HTML 报告。
继续学习
可进一步阅读 Understand Multiplatform project structure。Kotlin 生态中的 Kotest 提供另一种多平台测试选择,支持不同测试风格,以及 数据驱动测试与 基于属性的测试。
来源、许可证与静态审校
来源与许可:原文题为 Test your multiplatform app − tutorial,作者为 JetBrains Kotlin 文档贡献者,原文链接。来源仓库 kotlin-web-site 提供 Apache License 2.0;本文随附完整许可文本。本文包含中文翻译、结构调整、package 声明补全、断言顺序调整与边界说明,并配有原创源集图。译文依据另行授权刊载;原创图示单独授权使用。此归属不表示 JetBrains 为本文背书。
修订包括补全代码的 package 声明、统一 assertEquals 的 expected/actual 顺序、把截图信息转为步骤和结构图,并注明 JDK、Android 测试类型、iOS 工具链以及报告目录的版本边界。原逻辑与四个共享断言均保留。
静态风险:grep 的 pattern 是正则表达式,非法模式会抛异常;对不可信的大型或复杂模式没有超时与资源约束,不能直接把它作为生产搜索服务。CurrentRuntime 只提取有限版本片段。示例不含秘密、网络请求或破坏性命令。
使用边界:原文截图与运行结果是上游示例,不表示本文示例在任意项目环境中已经通过。采用代码前,应按当前 Kotlin Multiplatform、Gradle、Android Gradle Plugin、IDE、JDK 与 iOS 工具链版本核对源集名称、任务名及报告目录,并在目标环境验证。











暂无评论内容