【问题标题】:How do I run coroutines as blocking for unit testing?如何运行协程作为单元测试的阻塞?
【发布时间】:2019-05-22 19:56:50
【问题描述】:

我已经开始为我的 MVP Android 项目编写单元测试,但是我依赖于协程的测试间歇性地失败(通过日志记录和调试,我确认验证有时会提前发生,当然添加 delay 可以解决这个问题)

我尝试过使用runBlocking 进行包装,并且我从org.jetbrains.kotlinx:kotlinx-coroutines-test 中发现了Dispatchers.setMain(mainThreadSurrogate),但尝试了这么多组合到目前为止都没有取得任何成功。

abstract class CoroutinePresenter : Presenter, CoroutineScope {
    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main

    override fun onCreate() {
        super.onCreate()
        job = Job()
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
}

class MainPresenter @Inject constructor(private val getInfoUsecase: GetInfoUsecase) : CoroutinePresenter() {
    lateinit var view: View

    fun inject(view: View) {
        this.view = view
    }

    override fun onResume() {
        super.onResume()

        refreshInfo()
    }

    fun refreshInfo() = launch {
        view.showLoading()
        view.showInfo(getInfoUsecase.getInfo())
        view.hideLoading()
    }

    interface View {
        fun showLoading()
        fun hideLoading()

        fun showInfo(info: Info)
    }
}

class MainPresenterTest {
    private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread")

    private lateinit var presenter: MainPresenter
    private lateinit var view: MainPresenter.View

    val expectedInfo = Info()

    @Before
    fun setUp() {
        Dispatchers.setMain(mainThreadSurrogate)

        view = mock()

        val mockInfoUseCase = mock<GetInfoUsecase> {
            on { runBlocking { getInfo() } } doReturn expectedInfo
        }

        presenter = MainPresenter(mockInfoUseCase)
        presenter.inject(view)
        presenter.onCreate()
    }

    @Test
    fun onResume_RefreshView() {
        presenter.onResume()

        verify(view).showLoading()
        verify(view).showInfo(expectedInfo)
        verify(view).hideLoading()
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        mainThreadSurrogate.close()
    }
}

我认为 runBlocking 块应该强制所有子 coroutineScopes 在同一个线程上运行,强制它们在继续验证之前完成。

【问题讨论】:

  • 能否提供你Presenter的代码和完整的测试类代码。
  • @Sergey 我已按要求扩展了我的代码示例,希望对您有所帮助

标签: android kotlin junit mockito kotlin-coroutines


【解决方案1】:

CoroutinePresenter 类中,您使用的是Dispatchers.Main。您应该能够在测试中更改它。尝试执行以下操作:

  1. uiContext: CoroutineContext 参数添加到演示者的构造函数中:

    abstract class CoroutinePresenter(private val uiContext: CoroutineContext = Dispatchers.Main) : CoroutineScope {
    private lateinit var job: Job
    
    override val coroutineContext: CoroutineContext
        get() = uiContext + job
    
    //...
    }
    
    class MainPresenter(private val getInfoUsecase: GetInfoUsecase, 
                        private val uiContext: CoroutineContext = Dispatchers.Main 
    ) : CoroutinePresenter(uiContext) { ... }
    
  2. 更改MainPresenterTest 类以注入另一个CoroutineContext

    class MainPresenterTest {
        private lateinit var presenter: MainPresenter
    
        @Mock
        private lateinit var view: MainPresenter.View
    
        @Mock
        private lateinit var mockInfoUseCase: GetInfoUsecase
    
        val expectedInfo = Info()
    
        @Before
        fun setUp() {
            // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
            // inject the mocks in the test the initMocks method needs to be called.
            MockitoAnnotations.initMocks(this)
    
            presenter = MainPresenter(mockInfoUseCase, Dispatchers.Unconfined) // here another CoroutineContext is injected 
            presenter.inject(view)
            presenter.onCreate()
    }
    
        @Test
        fun onResume_RefreshView() = runBlocking {
            Mockito.`when`(mockInfoUseCase.getInfo()).thenReturn(expectedInfo)
    
            presenter.onResume()
    
            verify(view).showLoading()
            verify(view).showInfo(expectedInfo)
            verify(view).hideLoading()
        }
    }
    

【讨论】:

  • 重新架构,以便 CoroutineScope 能够被 Dispatchers.Unconfined 模拟,按预期工作。感谢您的帮助!
【解决方案2】:

@Sergey 的回答让我进一步阅读了Dispatchers.Unconfined,我意识到我没有充分利用Dispatchers.setMain()。在撰写本文时,请注意此解决方案是实验性

删除任何提及:

private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread") 

而是将主调度程序设置为

Dispatchers.setMain(Dispatchers.Unconfined)

这具有相同的结果。

一种不太惯用但可以作为权宜之计帮助任何人的方法是阻塞,直到所有子协程作业都完成(信用:https://stackoverflow.com/a/53335224/4101825):

this.coroutineContext[Job]!!.children.forEach { it.join() }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多