如何在Android Studio中运行Compose Multiplatform测试?命令行已可用
问题描述
为首个Compose Multiplatform项目配置测试环境,遵循官方文档操作后,命令行执行./gradlew :composeApp:connectedAndroidTest可成功运行测试,但在Android Studio(GUI)中运行时抛出空指针错误:
java.lang.NullPointerException: Cannot invoke "String.toLowerCase(java.util.Locale)" because "android.os.Build.FINGERPRINT" is null at androidx.compose.ui.test.AndroidComposeUiTestEnvironment.runTest(ComposeUiTest.android.kt:314) at androidx.compose.ui.test.ComposeUiTest_androidKt.runAndroidComposeUiTest(ComposeUiTest.android.kt:114) at androidx.compose.ui.test.ComposeUiTest_androidKt.runComposeUiTest(ComposeUiTest.android.kt:61) at androidx.compose.ui.test.ComposeUiTest_androidKt.runComposeUiTest$default(ComposeUiTest.android.kt:60) at cm.feature.game.play.TwisterViewModelTest.helloTestingCM(GameViewModelTest.kt:21) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.base/java.lang.reflect.Method.invoke(Unknown Source) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63) at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329) at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293) at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306) at org.junit.runners.ParentRunner.run(ParentRunner.java:413) at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:112) at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58) at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:40) at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:60) at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:52) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.base/java.lang.reflect.Method.invoke(Unknown Source) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94) at jdk.proxy1/jdk.proxy1.$Proxy2.processTestClass(Unknown Source) at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176) at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100) at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60) at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113) at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65) at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)
测试代码示例:
class GameViewModelTest { @OptIn(ExperimentalTestApi::class) @Test fun helloTestingCM() = runComposeUiTest { setContent { var text by remember { mutableStateOf("Hello") } Text( text = text, modifier = Modifier.testTag("text") ) Button( onClick = { text = "Compose" }, modifier = Modifier.testTag("button") ) { Text("Click ma") } } onNodeWithTag("text").assertTextEquals("Hello") onNodeWithTag("button").performClick() onNodeWithTag("text").assertTextEquals("Chompose") } }
问题原因
核心原因是Android Studio将Compose UI测试识别成了本地JVM单元测试,而非Android设备/模拟器上运行的Instrumented测试。本地单元测试在无Android系统环境的JVM中执行,导致android.os.Build相关字段(如FINGERPRINT)为null,进而触发空指针异常。而命令行的connectedAndroidTest任务默认针对设备/模拟器运行Instrumented测试,因此能正常执行。
解决方案
1. 确认测试文件目录位置
Compose UI测试属于Instrumented测试,必须放在src/androidTest/kotlin/目录下,而非src/test/kotlin/(后者用于本地单元测试)。如果文件位置错误,Android Studio会默认按本地单元测试运行。
2. 选择正确的运行方式
- 右键测试方法或类,在弹出菜单中选择**"Run '<测试方法名>' on Android Device"**(或类似选项,确保目标是设备/模拟器)。
- 若默认菜单选项不正确,可手动创建运行配置:
- 打开
Run > Edit Configurations - 点击
+号,选择Android Instrumented Tests - 指定测试所属模块,选择目标测试类/方法,保存后运行。
- 打开
3. 验证依赖配置
确保模块的build.gradle.kts中,Compose测试相关依赖添加在androidTestImplementation和debugImplementation下:
android { // ...其他配置 } dependencies { // Instrumented测试依赖 androidTestImplementation("androidx.compose.ui:ui-test-junit4:${composeVersion}") debugImplementation("androidx.compose.ui:ui-test-manifest:${composeVersion}") // ...其他依赖 }
注意不要将这些依赖误加到testImplementation(本地单元测试依赖块)中。
额外提示
测试代码中的断言存在一处笔误:assertTextEquals("Chompose")应改为assertTextEquals("Compose"),否则测试最终会断言失败,不过这和当前空指针错误无关。
内容的提问来源于stack exchange,提问作者Paul Frank Allan

