Issue
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.
Solution
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 anotherCoroutineContext
: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() } }
Answered By - Sergey
Answer Checked By - David Goodson (JavaFixing Volunteer)