I've begun writing unit tests for my MVP Android project, but my tests dependent on coroutines intermittently fail (through logging and debugging I've confirmed verify sometimes occurs early, adding delay
fixes this of course)
I've tried wrapping with runBlocking
and I've discovered Dispatchers.setMain(mainThreadSurrogate)
from org.jetbrains.kotlinx:kotlinx-coroutines-test
, but trying so many combinations hasn't yielded any success so far.
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()
}
}
I believe the runBlocking
blocks should be forcing all child coroutineScopes
to run on the same thread, forcing them to complete before moving on to verification.
In CoroutinePresenter
class you are using Dispatchers.Main
. You should be able to change it in the tests. Try to do the following:
Add uiContext: CoroutineContext
parameter to your presenters' constructor:
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) { ... }
Change MainPresenterTest
class to inject another 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's answer caused me to read further into Dispatchers.Unconfined
and I realised that I was not using Dispatchers.setMain()
to its fullest extent. At the time of writing, note this solution is experimental.
By removing any mention of:
private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread")
and instead setting the main dispatcher to
Dispatchers.setMain(Dispatchers.Unconfined)
This has the same result.
A less idiomatic method but one that may assist anyone as a stop-gap solution is to block until all child coroutine jobs have completed (credit: https://stackoverflow.com/a/53335224/4101825):
this.coroutineContext[Job]!!.children.forEach { it.join() }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With