我如何 运行 协同程序作为单元测试的阻塞?
How do I run coroutines as blocking for unit testing?
我已经开始为我的 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
在同一个线程上 运行,迫使它们在继续验证之前完成。
在 CoroutinePresenter
class 中,您正在使用 Dispatchers.Main
。您应该能够在测试中更改它。尝试执行以下操作:
将 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) { ... }
更改MainPresenterTest
class注入另一个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()
}
}
@Sergey 的回答让我进一步阅读了 Dispatchers.Unconfined
,我意识到我并没有充分利用 Dispatchers.setMain()
。在撰写本文时,请注意此解决方案是 实验性的 。
通过删除任何提及:
private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread")
并将主调度程序设置为
Dispatchers.setMain(Dispatchers.Unconfined)
这有相同的结果。
一种不那么惯用但可以帮助任何人作为权宜之计的方法是阻塞,直到所有子协程作业完成(信用:):
this.coroutineContext[Job]!!.children.forEach { it.join() }
我已经开始为我的 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
在同一个线程上 运行,迫使它们在继续验证之前完成。
在 CoroutinePresenter
class 中,您正在使用 Dispatchers.Main
。您应该能够在测试中更改它。尝试执行以下操作:
将
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) { ... }
更改
MainPresenterTest
class注入另一个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() } }
@Sergey 的回答让我进一步阅读了 Dispatchers.Unconfined
,我意识到我并没有充分利用 Dispatchers.setMain()
。在撰写本文时,请注意此解决方案是 实验性的 。
通过删除任何提及:
private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread")
并将主调度程序设置为
Dispatchers.setMain(Dispatchers.Unconfined)
这有相同的结果。
一种不那么惯用但可以帮助任何人作为权宜之计的方法是阻塞,直到所有子协程作业完成(信用:
this.coroutineContext[Job]!!.children.forEach { it.join() }