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
相关产品推荐
相关产品推荐

