测试 Kotlin 多平台应用:共享测试与平台专用测试

测试 Kotlin 多平台应用:共享测试与平台专用测试

原文:Test your multiplatform app − tutorial。作者:JetBrains Kotlin 文档贡献者(原页无个人署名)。原页更新日期为 2026 年 10 月 1 日,本文依据该页内容翻译整理。

本教程介绍如何在 Kotlin Multiplatform 应用中创建、配置和运行测试。多平台测试分两类:共享代码测试可通过支持的框架在目标平台运行;平台专用测试验证各平台独有逻辑,并可使用平台框架更丰富的 API、断言及扩展能力。多平台项目同时支持两类测试。我们先从简单的共享单元测试开始,再扩展到同时包含共享和平台逻辑的例子。

开始前,应熟悉 Kotlin Multiplatform 项目结构,以及 JUnit 等常见单元测试框架的基础知识。Android 本地主机测试需要合适的 JDK 与 Android 开发配置;iOS 测试需要 Mac 和 Xcode。

commonMain对应commonTest;Android实现位于androidMain并由androidHostTest在本地JVM测试;iOS实现位于iosMain并由iosTest通过Kotlin Native测试运行器执行
技术示意:源集、实现与测试运行环境的对应关系。未完纪依据 Kotlin 官方教程绘制。

测试一个简单的多平台项目

创建项目

  1. 按 快速入门配置 Kotlin Multiplatform 开发环境。
  2. 在 IntelliJ IDEA 中选择 File → New → Project。
  3. 左侧选择 Kotlin Multiplatform。
  4. Name 填 KMP testing,Project ID 填 kmp.project.testing。
  5. 选择 Android 目标;使用 Mac 时也选择 iOS。选择 Do not share UI。
  6. 取消 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 来自项目版本目录,需与模板生成的别名一致。然后:

  1. 右键 sharedLogic/src,选择 New → Directory。
  2. 输入并选择 commonTest/kotlin。commonTest 存放共享测试。
  3. 在该目录下创建包 common.example.search。
  4. 创建测试文件 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。构造器接收名称字符串和可空版本字符串;版本存在时,仅提取开头的数字部分。

  1. 在 commonMain/kotlin 下创建包 org.kmp.testing。
  2. 创建 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。

  1. 在 commonTest/kotlin 下创建同名包 org.kmp.testing。
  2. 创建 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 的实现和本地测试

  1. 在 androidMain/kotlin 下创建包 org.kmp.testing。
  2. 创建 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)
}
  1. 右键 sharedLogic/src,选择 New → Directory,输入并选择 androidHostTest/kotlin。
  2. 在其中创建包 org.kmp.testing。
  3. 创建 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 的实现和测试

  1. 在 iosMain/kotlin 下创建包 org.kmp.testing。
  2. 创建 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)
}
  1. 右键 sharedLogic/src,创建 iosTest/kotlin。
  2. 在其中创建包 org.kmp.testing。
  3. 创建 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 工具链版本核对源集名称、任务名及报告目录,并在目标环境验证。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容