You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试包含rememberLauncherForActivityResult的Jetpack Compose组件?

测试调用rememberLauncherForActivityResult的Compose可组合项解决方案

核心思路

无需依赖真实外部Activity启动,而是模拟Activity Result的回调结果,验证可组合项在收到回调后的行为是否符合预期——因为Compose测试规则无法跨Activity交互,直接测试外部Activity不在其能力范围内。

步骤1:重构可组合项,提升可测试性

先抽离回调逻辑,让可组合项的行为更易被测试:

import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.compose.runtime.Composable
import androidx.compose.material3.Button
import androidx.compose.material3.Text

@Composable
fun PickFileButton(
    onFileSelected: (Uri?) -> Unit = {} // 抽离回调,方便测试时注入验证逻辑
) {
    val launcher = rememberLauncherForActivityResult(CreateDocument("text/plain")) { uri ->
        onFileSelected(uri)
    }
    Button(onClick = { launcher.launch("default_file_name.txt") }) {
        Text("Pick File")
    }
}

步骤2:用ActivityResultRegistry模拟回调

创建测试用的ActivityResultRegistry,手动触发回调,跳过真实的CreateDocument Activity启动流程:

import android.net.Uri
import androidx.activity.result.ActivityResultRegistry
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.core.app.ApplicationProvider
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

class TestPickFileButton {
    @get:Rule val composeTestRule = createComposeRule()

    @Test
    fun testPickFileButton_triggersCallbackOnResult() {
        // 1. 创建模拟回调,用于验证是否被触发
        val mockOnFileSelected: (Uri?) -> Unit = mock()
        
        // 2. 自定义测试用Registry,拦截launch调用并直接触发回调
        val testRegistry = object : ActivityResultRegistry() {
            override fun <I, O> register(
                key: String,
                lifecycleOwner: androidx.lifecycle.LifecycleOwner,
                contract: androidx.activity.result.contract.ActivityResultContract<I, O>,
                callback: androidx.activity.result.ActivityResultCallback<O>
            ): androidx.activity.result.ActivityResultLauncher<I> {
                return object : androidx.activity.result.ActivityResultLauncher<I>() {
                    override fun launch(input: I, options: androidx.activity.result.ActivityResultLauncher.IgnoreOptions?) {
                        // 模拟返回测试用Uri
                        val testUri = Uri.parse("content://test/default_file_name.txt")
                        callback.onActivityResult(testUri as O)
                    }

                    override fun unregister() {}
                    override fun getContract(): androidx.activity.result.contract.ActivityResultContract<I, O> = contract
                }
            }
        }

        // 3. 设置Compose内容,注入测试用Registry
        composeTestRule.setContent {
            androidx.activity.compose.LocalActivityResultRegistryProvider(testRegistry) {
                PickFileButton(onFileSelected = mockOnFileSelected)
            }
        }

        // 4. 执行点击操作
        composeTestRule.onNodeWithText("Pick File").performClick()

        // 5. 验证回调是否被正确触发
        val expectedUri = Uri.parse("content://test/default_file_name.txt")
        verify(mockOnFileSelected).invoke(expectedUri)
    }
}

步骤3:验证文件创建逻辑(若有)

如果回调中包含文件写入等业务逻辑,将这部分逻辑抽离到独立类,通过Mock验证方法调用:

// 抽离的文件处理类
import android.content.Context
import android.net.Uri
class FileManager(private val context: Context) {
    fun writeFile(uri: Uri, content: String) {
        context.contentResolver.openOutputStream(uri)?.use {
            it.write(content.toByteArray())
        }
    }
}

// 修改可组合项,注入FileManager
import androidx.compose.ui.platform.LocalContext
@Composable
fun PickFileButton(
    fileManager: FileManager = FileManager(LocalContext.current),
    defaultContent: String = "test content"
) {
    val launcher = rememberLauncherForActivityResult(CreateDocument("text/plain")) { uri ->
        uri?.let { fileManager.writeFile(it, defaultContent) }
    }
    Button(onClick = { launcher.launch("default_file_name.txt") }) {
        Text("Pick File")
    }
}

// 测试文件创建逻辑
@Test
fun testPickFileButton_writesFileOnSelection() {
    val mockFileManager: FileManager = mock()
    val testUri = Uri.parse("content://test/default_file_name.txt")

    val testRegistry = object : ActivityResultRegistry() {
        override fun <I, O> register(...) = object : androidx.activity.result.ActivityResultLauncher<I>() {
            override fun launch(input: I, options: androidx.activity.result.ActivityResultLauncher.IgnoreOptions?) {
                callback.onActivityResult(testUri as O)
            }

            override fun unregister() {}
            override fun getContract(): androidx.activity.result.contract.ActivityResultContract<I, O> = contract
        }
    }

    composeTestRule.setContent {
        androidx.activity.compose.LocalActivityResultRegistryProvider(testRegistry) {
            PickFileButton(fileManager = mockFileManager)
        }
    }

    composeTestRule.onNodeWithText("Pick File").performClick()

    // 验证文件写入方法是否被正确调用
    verify(mockFileManager).writeFile(testUri, "test content")
}

关键注意点

  • 不要尝试用Compose测试规则控制外部Activity,这不属于它的测试范畴
  • 聚焦验证可组合项自身的逻辑:点击是否触发launch、回调是否被正确处理
  • 抽离业务逻辑到独立类,既降低耦合,也让测试更简单

内容的提问来源于stack exchange,提问作者David Hadley

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 01:10:29