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

Compose UI测试+协程问题:登录测试未等待ViewModel执行完成

登录流程Android测试断言失败问题解决方案

问题描述

我正在编写AndroidTest点击测试验证登录流程,步骤为打开页面、点击登录按钮、输入邮箱/密码、点击实际登录按钮。但测试未等待ViewModel中.login(email, pass).collect()逻辑执行完成,导致sessionManager.hasValidSession()始终返回false,断言失败。

尝试过启用MainDispatcherRule统一协程调度器,但会导致Compose UI陷入无限非空闲状态最终超时;也试过advanceUntilIdle和添加延迟,未尝试Mock ViewModel(API调用已通过MockApi模拟)。

解决方案

1. 修复协程调度器与IdlingResource的冲突

问题根源:启用MainDispatcherRule后,Compose UI协程和ViewModel业务协程使用TestDispatcher,但现有IdlingResource未适配TestDispatcher,导致Espresso无法正确识别空闲状态。

  • 临时方案:暂时注释BaseTest中的IdlingResource注册逻辑,避免与TestDispatcher冲突。
  • 长期方案:实现适配TestDispatcher的自定义IdlingResource,确保协程执行状态能被Espresso识别。

2. 正确配置测试调度器并等待协程执行完成

使用StandardTestDispatcher替代UnconfinedTestDispatcher,它能让协程按顺序执行,便于测试控制;同时在关键步骤后调用advanceUntilIdle()推进时间,确保所有协程逻辑执行完毕。

修改后的测试代码

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class LoginUserTest : BaseTest(hasValidSession = false) {
    private val sessionManager: SessionManager by inject()

    @get:Rule
    val mainRule = MainDispatcherRule(StandardTestDispatcher())

    @Test
    fun start_app_login_user() = runTest {
        // 点击启动页登录按钮
        composeTestRule.onNodeWithTag(TestTagID.StartLoginButton).performClick()
        composeTestRule.advanceUntilIdle()

        // 输入邮箱
        composeTestRule.onNodeWithTag(TestTagID.EmailLoginEmail).apply {
            performTextInput("mail.mail@gmail.com")
            performImeAction()
        }
        // 输入密码
        composeTestRule.onNodeWithTag(TestTagID.EmailLoginPassword).apply {
            performTextInput("Hello123!!")
            performImeAction()
        }
        composeTestRule.advanceUntilIdle()

        // 点击登录按钮
        composeTestRule.onNodeWithTag(TestTagID.EmailLoginButton).apply {
            assertIsEnabled()
            performClick()
        }

        // 推进时间直到所有协程执行完成
        advanceUntilIdle()

        // 断言会话已创建
        assertTrue { sessionManager.hasValidSession() }
    }
}

修改后的BaseTest(临时注释IdlingResource)

open class BaseTest(private val hasValidSession: Boolean = true) : AutoCloseKoinTest() {

    private var scenario: ActivityScenario<MainActivity>? = null

    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComposeTestActivity>()

    @get:Rule
    val mockProvider = MockProviderRule.create { mockkClass(it, relaxed = true) }

    private val sessionManager: SessionManager by inject()
    private val loadableRepository: LoadableRepo by inject()
    private val idlingResources: Array<EspressoIdlingResource> by lazy { arrayOf(loadableRepository) }
    private val composeIdlingResources: List<ComposeIdlingResource> by lazy {
        idlingResources.map {
            object : ComposeIdlingResource {
                override val isIdleNow: Boolean get() = it.isIdleNow
            }
        }
    }

    @Before
    open fun setUp() {
        declare<MainApi> { MockApi(get()) } // 确保所有API调用走Mock端点

        sessionManager.setAuthToken(AuthResponse.mock().takeIf { hasValidSession })
        Locale.setDefault(Locale.US)

        // 暂时注释IdlingResource,避免与TestDispatcher冲突
        // IdlingRegistry.getInstance().register(*idlingResources)
        // composeIdlingResources.forEach { composeTestRule.registerIdlingResource(it) }
        scenario = ActivityScenario.launch(MainActivity::class.java)
    }

    @After
    open fun tearDown() {
        scenario?.close()
        // IdlingRegistry.getInstance().unregister(*idlingResources)
        // composeIdlingResources.forEach { composeTestRule.unregisterIdlingResource(it) }
    }
}

关键说明

  • StandardTestDispatcher能让协程按预期顺序执行,避免UnconfinedTestDispatcher导致的状态不同步问题。
  • advanceUntilIdle()会自动推进时间,直到所有调度在TestDispatcher上的协程执行完成,确保登录逻辑和sessionManager的状态更新都已完成。
  • 若advanceUntilIdle()无效,可尝试用advanceTimeBy(1000)手动推进足够时间(根据实际业务逻辑调整时长)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 18:44:52