如何将协程作为单元测试的阻塞?

时间:2018-12-22 23:42:11

标签: android junit kotlin mockito kotlinx.coroutines

我已经开始为我的MVP Android项目编写单元测试,但是我的依赖于协程的测试间歇性地失败了(通过记录和调试,我已经确认有时会提前进行,当然添加 'INSERT INTO table ({target list}) VALUES({values}) ON DUPLICATE KEY UPDATE val1={value} , val2={value},...' 可以解决此问题)< / p>

我尝试用delay包装,并且从runBlocking中发现了Dispatchers.setMain(mainThreadSurrogate),但是尝试如此多的组合到目前为止并没有取得任何成功。

org.jetbrains.kotlinx:kotlinx-coroutines-test

我认为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在同一线程上运行,并迫使它们在进行验证之前完成。

2 个答案:

答案 0 :(得分:5)

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()
        }
    }
    

答案 1 :(得分: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() }