Android协程主线程Dispatcher导致单元测试失败求助
问题描述
首次尝试为使用协程的Android代码编写单元测试,但测试失败。已添加CoroutineTestRule配置StandardTestDispatcher,但似乎无效。Presenter类中通过withContext(Dispatchers.Main)切换线程更新View,移除该行测试即可通过。以下是相关代码,求解决思路。
Presenter代码
class PostsActivityPresenter( private val apiHelper: ApiFactory ) : CoroutineScope, PostsContract.Presenter { private var view: PostsContract.View? = null private val job: Job = Job() override val coroutineContext: CoroutineContext = job + Dispatchers.IO override fun attachView(view: PostsContract.View?) { this.view = view } override fun getPostsandUserInfo() { launch { val posts = apiHelper.getPosts() val users = apiHelper.getUsers() val postsWithUserDetails = mutableListOf<PostsWithUserInfo>() for (post in posts) { users.firstOrNull { it.id == post.userId }?.let { user -> postsWithUserDetails.add(PostsWithUserInfo(post, user)) } } withContext(Dispatchers.Main) { ///// 移除该行测试即可通过 view?.displayPosts(postsWithUserDetails) } } } override fun detachView() { job.cancel() view = null } }
单元测试代码
class PostsPresenterTest { @RelaxedMockK private lateinit var apiHelper: ApiHelper @io.mockk.impl.annotations.MockK private lateinit var view: PostsContract.View @ExperimentalCoroutinesApi @get:Rule var coroutinesTestRule = CoroutineTestRule() private lateinit var subject: PostsActivityPresenter private val posts = listOf( Post( id = 1, userId = 12, title = "title 1", body = "body 1" ) ) private val users = listOf( User( name = "Name1", username = "Username1", email = "email1@gmail.com", id = 12 ), User( name = "Name2", username = "Username2", email = "email2@gmail.com", id = 13 ) ) @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) subject = PostsActivityPresenter(apiHelper) } @ExperimentalCoroutinesApi @Test fun `test if posts is being called`() = runBlocking { // arrange coEvery { apiHelper.getPosts() }.returns(posts) coEvery { apiHelper.getUsers() }.returns(users) every { view.displayPosts(any()) }.returns(Unit) // act subject.attachView(view) subject.getPostsandUserInfo() // assert coVerify { view.displayPosts( listOf( PostsWithUserInfo( post = Post( userId = 12, id = 1, title = "title 1", body = "body 1" ), user = User( id = 12, name = "Name1", username = "Username1", email = "email1@gmail.com" ) ) ) ) } } } @ExperimentalCoroutinesApi class CoroutineTestRule(private val testDispatcher: TestDispatcher = StandardTestDispatcher()) : TestWatcher() { override fun starting(description: Description) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { super.finished(description) Dispatchers.resetMain() testDispatcher.cleanupTestCoroutines() } }
解决思路
问题核心:
StandardTestDispatcher是惰性调度器,不会立即执行队列中的任务。withContext(Dispatchers.Main)里的代码会被放到调度器队列中,但测试执行到coVerify时,这部分代码还没运行,导致断言失败。快速修复方案:在测试的
act步骤后手动触发调度器执行所有待处理任务:// act subject.attachView(view) subject.getPostsandUserInfo() // 新增:触发Main调度器的任务执行 coroutinesTestRule.testDispatcher.scheduler.runCurrent()更省心的方案:改用
UnconfinedTestDispatcher,它会立即执行协程任务,无需手动触发。修改CoroutineTestRule的默认参数:class CoroutineTestRule(private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()) : TestWatcher() { // ... 原有代码不变 }推荐的架构优化:让Presenter的调度器可注入,避免硬编码
Dispatchers.IO和Dispatchers.Main,彻底解耦测试与生产环境的调度器:class PostsActivityPresenter( private val apiHelper: ApiFactory, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main ) : CoroutineScope, PostsContract.Presenter { private val job: Job = Job() override val coroutineContext: CoroutineContext = job + ioDispatcher override fun getPostsandUserInfo() { launch { // ... 原有业务逻辑不变 withContext(mainDispatcher) { view?.displayPosts(postsWithUserDetails) } } } }测试时传入测试调度器:
@Before fun setUp() { MockKAnnotations.init(this, relaxed = true) val testDispatcher = StandardTestDispatcher() subject = PostsActivityPresenter(apiHelper, testDispatcher, testDispatcher) }额外优化:把测试中的
runBlocking换成runTest(来自kotlinx-coroutines-test库),它专为协程测试设计,会自动管理测试调度器的生命周期,代码更简洁可靠。
内容的提问来源于stack exchange,提问作者srisindhu saride
相关产品推荐
相关产品推荐

