测试您的多平台应用 – 教程
本教程使用 IntelliJ IDEA,但您也可以在 Android Studio 中按照本教程进行操作 – 这两个 IDE 共享相同的核心功能和 Kotlin Multiplatform 支持。
在本教程中,您将学习如何在 Kotlin Multiplatform 应用程序中创建、配置和运行测试。
多平台项目的测试可以分为两类:
- 公共代码的测试。这些测试可以使用任何支持的框架在任何平台上运行。
- 平台特定代码的测试。这些对于测试平台特定逻辑至关重要。它们使用平台特定的框架,并可以受益于其附加功能,例如更丰富的 API 和更广泛的断言。
多平台项目支持这两个类别。本教程首先向您展示如何在一个简单的 Kotlin Multiplatform 项目中为公共代码设置、创建和运行单元测试。然后,您将处理一个更复杂的示例,该示例需要针对公共代码和平台特定代码进行测试。
本教程假设您熟悉:
测试简单的多平台项目
创建项目
在 IntelliJ IDEA 中,选择 File | New | Project。
在左侧面板中,选择 Kotlin Multiplatform。
在 New Project 窗口中指定以下字段:
- Name: KMP testing
- Project ID: kmp.project.testing
选择 Android 目标。 如果您使用的是 Mac,请同时选择 iOS。确保已选择 Do not share UI 选项。
取消选择 Include tests 并点击 Create。

编写代码
在 sharedLogic/src/commonMain/kotlin 目录中,创建一个新的 common.example.search 软件包。 在此软件包中,创建一个 Kotlin 文件 Grep.kt,并添加以下函数:
fun grep(lines: List<String>, pattern: String, action: (String) -> Unit) {
val regex = pattern.toRegex()
lines.filter(regex::containsMatchIn)
.forEach(action)
}此函数旨在模拟 UNIX grep 命令。在这里,该函数接收多行文本、一个用作正则表达式的模式,以及一个在每当某行匹配模式时调用的函数。
添加测试
现在,让我们测试公共代码。一个必不可少的部分是公共测试的源集,它将 kotlin.test API 库作为依赖项。
在
sharedLogic/build.gradle.kts文件中,检查是否存在对kotlin.test库的依赖:kotlinsourceSets { //... commonTest.dependencies { implementation(libs.kotlin.test) } }commonTest源集存储所有公共测试。您需要在项目中创建一个同名目录:- 右键点击
sharedLogic/src目录,然后选择 New | Directory。IDE 会显示一个选项列表。 - 开始输入
commonTest/kotlin路径以缩小选择范围,然后从列表中选择它:

- 右键点击
在
commonTest/kotlin目录中,创建一个新的common.example.search软件包。在此软件包中,创建
Grep.kt文件并使用以下单元测试更新它:kotlinimport 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") } } }
如您所见,导入的注解和断言既不是平台特定的,也不是框架特定的。当您稍后运行此测试时,平台特定的框架将提供测试运行程序。
探索 kotlin.test API
kotlin.test 库提供了与平台无关的注解和断言,供您在测试中使用。Test 等注解映射到所选框架提供的注解或其最接近的等效项。
断言通过 Asserter 接口的实现来执行。此接口定义了测试中常用的不同检查。该 API 有一个默认实现,但通常您将使用框架特定的实现。
例如,JVM 上支持 JUnit 4、JUnit 5 和 TestNG 框架。在 Android 上,对 assertEquals() 的调用可能会导致对 asserter.assertEquals() 的调用,其中 asserter 对象是 JUnit4Asserter 的一个实例。在 iOS 上,Asserter 类型的默认实现与 Kotlin/Native 测试运行程序配合使用。
运行测试
您可以通过以下方式执行测试:
- 使用装订区域中的 Run 图标运行
shouldFindMatches()测试函数。 - 使用测试文件的上下文菜单运行该文件。
- 使用装订区域中的 Run 图标运行
GrepTest测试类。
还有一个方便的快捷键 /。无论您选择哪种选项,都会看到一个运行测试的目标列表:

对于 android 选项,测试使用 JUnit 4 运行。对于 iosSimulatorArm64,Kotlin 编译器会检测测试注解并创建一个由 Kotlin/Native 自己的测试运行程序执行的“测试二进制文件”。
以下是成功运行测试生成的输出示例:

处理更复杂的项目
为公共代码编写测试
您已经使用 grep() 函数创建了一个公共代码测试。现在,让我们考虑一个使用 CurrentRuntime 类的更高级公共代码测试。此类包含执行代码的平台的详细信息。例如,对于在本地 JVM 上运行的 Android 单元测试,它可能具有值 "OpenJDK" 和 "17.0"。
CurrentRuntime 的实例应使用作为字符串的平台名称和版本创建,其中版本是可选的。当存在版本时,如果可用,您只需要字符串开头的数字。
在
commonMain/kotlin目录中,创建一个新的org.kmp.testing软件包。在此软件包中,创建
CurrentRuntime.kt文件并使用以下实现更新它:kotlinclass 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" } }在
commonTest/kotlin目录中,创建一个新的org.kmp.testing软件包。在此软件包中,创建
CurrentRuntimeTest.kt文件并使用以下平台和框架无关的测试更新它:kotlinimport 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 中提供的任何方式运行此测试。
添加平台特定测试
这里为了简洁明了,使用了预期声明与实际声明机制。在更复杂的代码中,更好的方法是使用接口和工厂函数。
现在您已经有了编写公共代码测试的经验,让我们探索为 Android 和 iOS 编写平台特定测试。
要创建 CurrentRuntime 实例,请在公共 CurrentRuntime.kt 文件中声明如下函数:
expect fun determineCurrentRuntime(): CurrentRuntime该函数应对每个受支持的平台具有单独的实现。否则,构建将失败。除了在每个平台上实现此函数外,您还应该提供测试。让我们为 Android 和 iOS 创建它们。
针对 Android
在
androidMain/kotlin目录中,创建一个新的org.kmp.testing软件包。在此软件包中,创建
AndroidRuntime.kt文件,并使用预期函数determineCurrentRuntime()的实际实现来更新它:kotlinactual fun determineCurrentRuntime(): CurrentRuntime { val name = System.getProperty("java.vm.name") ?: "Android" val version = System.getProperty("java.version") return CurrentRuntime(name, version) }在
sharedLogic/src目录中创建一个测试目录:右键点击
sharedLogic/src目录,然后选择 New | Directory。IDE 会显示一个选项列表。开始输入
androidHostTest/kotlin路径以缩小选择范围,然后从列表中选择它:
在
androidHostTest/kotlin目录中,创建一个新的org.kmp.testing软件包。在此软件包中,创建
AndroidRuntimeTest.kt文件并使用以下 Android 测试更新它。为了使测试通过,请确保设置运行时的实际名称和版本(但查看测试如何失败也是有用的):kotlinimport kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals class AndroidRuntimeTest { @Test fun shouldDetectAndroid() { val runtime = determineCurrentRuntime() assertContains(runtime.name, "OpenJDK") assertEquals(runtime.version, "21.0") } }
Android 特定的测试在本地 JVM 上运行,这看起来可能很奇怪。这是因为这些测试在当前机器上作为本地单元测试运行。如 Android Studio 文档中所述,这些测试与在设备或模拟器上运行的受检测测试不同。
您可以向项目添加其他类型的测试。要了解受检测测试,请参阅此 Touchlab 指南。
针对 iOS
In the
iosMain/kotlin目录中,创建一个新的org.kmp.testing目录。在此目录中,创建
IOSRuntime.kt文件,并使用预期函数determineCurrentRuntime()的实际实现来更新它:kotlinimport 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目录中创建一个新目录:- 右键点击
sharedLogic/src目录,然后选择 New | Directory。IDE 会显示一个选项列表。 - 开始输入
iosTest/kotlin路径以缩小选择范围,然后从列表中选择它:
- 右键点击
在
iosTest/kotlin目录中,创建一个新的org.kmp.testing目录。在此目录中,创建
IOSRuntimeTest.kt文件并使用以下 iOS 测试更新它:kotlinimport kotlin.test.Test import kotlin.test.assertEquals class IOSRuntimeTest { @Test fun shouldDetectOS() { val runtime = determineCurrentRuntime() assertEquals(runtime.name, "ios") assertEquals(runtime.version, "unknown") } }
运行多个测试并分析报告
在这个阶段,您已经拥有了公共、Android 和 iOS 实现的代码以及它们的测试。您项目中的目录结构应该如下所示:

您可以从上下文菜单运行单个测试或使用快捷键。另一个选项是使用 Gradle 任务。例如,如果您运行 allTests Gradle 任务,项目中的每个测试都将使用相应的测试运行程序运行:

运行测试时,除了 IDE 中的输出外,还会生成 HTML 报告。您可以在 sharedLogic/build/reports/tests 目录中找到它们:

运行 allTests 任务并检查其生成的报告:
allTests/index.html文件包含公共测试和 iOS 测试的合并报告(iOS 测试依赖于公共测试,并在公共测试之后运行)。testDebugUnitTest和testReleaseUnitTest文件夹包含两个默认 Android 构建变体的报告。(目前,Android 测试报告不会自动与allTests报告合并。)

在多平台项目中使用测试的规则
您现在已经在 Kotlin Multiplatform 应用程序中创建、配置并执行了测试。在未来的项目中处理测试时,请记住:
- 编写公共代码的测试时,仅使用多平台库,例如 kotlin.test。将依赖项添加到
commonTest源集。 kotlin.testAPI 中的Asserter类型只能间接使用。虽然Asserter实例是可见的,但您不需要在测试中使用它。- 始终保持在测试库 API 范围内。幸运的是,编译器和 IDE 会阻止您使用框架特定的功能。
- 虽然使用哪个框架运行
commonTest中的测试并不重要,但最好使用您打算使用的每个框架运行测试,以检查您的开发环境是否已正确设置。 - 考虑物理差异。例如,滚动惯性和摩擦值因平台和设备而异,因此设置相同的滚动速度可能会导致不同的滚动位置。请始终在目标平台上测试您的组件以确保预期行为。
- 编写平台特定代码的测试时,您可以使用相应框架的功能,例如注解和扩展。
- 您可以从 IDE 运行测试,也可以使用 Gradle 任务。
- 运行测试时,会自动生成 HTML 测试报告。