I have two coroutines tests that both pass when run individually, but if I run them together the second one always fails (even if I switch them around!). The error I get is:
Wanted but not invoked: observer.onChanged([SomeObject(someValue=test2)]); Actually, there were zero interactions with this mock.
There's probably something fundamental I don't understand about coroutines (or testing in general) and doing something wrong.
If I debug the tests I find that the failing test is not waiting for the inner runBlocking
to complete. Actually the reason I have the inner runBlocking
in the first place is to solve this exact problem and it seemed to work for individual tests.
Any ideas as to why this might be happening?
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class ViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var mainThreadSurrogate: ExecutorCoroutineDispatcher
@Mock
lateinit var repository: DataSource
@Mock
lateinit var observer: Observer<List<SomeObject>>
private lateinit var viewModel: SomeViewModel
@Before
fun setUp() {
mainThreadSurrogate = newSingleThreadContext("UI thread")
Dispatchers.setMain(mainThreadSurrogate)
viewModel = SomeViewModel(repository)
}
@After
fun tearDown() {
Dispatchers.resetMain()
mainThreadSurrogate.close()
}
@Test
fun `loadObjects1 should get objects1`() = runBlocking {
viewModel.someObjects1.observeForever(observer)
val expectedResult = listOf(SomeObject("test1"))
`when`(repository.getSomeObjects1Async())
.thenReturn(expectedResult)
runBlocking {
viewModel.loadSomeobjects1()
}
verify(observer).onChanged(listOf(SomeObject("test1")))
}
@Test
fun `loadObjects2 should get objects2`() = runBlocking {
viewModel.someObjects2.observeForever(observer)
val expectedResult = listOf(SomeObject("test2"))
`when`(repository.getSomeObjects2Async())
.thenReturn(expectedResult)
runBlocking {
viewModel.loadSomeObjects2()
}
verify(observer).onChanged(listOf(SomeObject("test2")))
}
}
class SomeViewModel constructor(private val repository: DataSource) :
ViewModel(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private var objects1Job: Job? = null
private var objects2Job: Job? = null
val someObjects1 = MutableLiveData<List<SomeObject>>()
val someObjects2 = MutableLiveData<List<SomeObject>>()
fun loadSomeObjects1() {
objects1Job = launch {
val objects1Result = repository.getSomeObjects1Async()
objects1.value = objects1Result
}
}
fun loadSomeObjects2() {
objects2Job = launch {
val objects2Result = repository.getSomeObjects2Async()
objects2.value = objects2Result
}
}
override fun onCleared() {
super.onCleared()
objects1Job?.cancel()
objects2Job?.cancel()
}
}
class Repository(private val remoteDataSource: DataSource) : DataSource {
override suspend fun getSomeObjects1Async(): List<SomeObject> {
return remoteDataSource.getSomeObjects1Async()
}
override suspend fun getSomeObjects2Async(): List<SomeObject> {
return remoteDataSource.getSomeObjects2Async()
}
}
When you use launch
, you're creating a coroutine which will execute asynchronously. Using runBlocking
does nothing to affect that.
Your tests are failing because the stuff inside your launches will happen, but hasn't happened yet.
The simplest way to ensure that your launches have executed before doing any assertions is to call .join()
on them.
fun someLaunch() : Job = launch {
foo()
}
@Test
fun `test some launch`() = runBlocking {
someLaunch().join()
verify { foo() }
}
Instead of saving off individual Jobs
in your ViewModel
, in onCleared()
you can implement your CoroutineScope
like so:
class MyViewModel : ViewModel(), CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext : CoroutineContext
get() = job + Dispatchers.Main
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
All launches which happen within a CoroutineScope
become children of that CoroutineScope
, so if you cancel that job
(which is effectively cancelling the CoroutineScope
), then you cancel all coroutines executing within that scope.
So, once you've cleaned up your CoroutineScope
implementation, you can make your ViewModel
functions just return Job
s:
fun loadSomeObjects1() = launch {
val objects1Result = repository.getSomeObjects1Async()
objects1.value = objects1Result
}
and now you can test them easily with a .join()
:
@Test
fun `loadObjects1 should get objects1`() = runBlocking {
viewModel.someObjects1.observeForever(observer)
val expectedResult = listOf(SomeObject("test1"))
`when`(repository.getSomeObjects1Async())
.thenReturn(expectedResult)
viewModel.loadSomeobjects1().join()
verify(observer).onChanged(listOf(SomeObject("test1")))
}
I also noticed that you're using Dispatchers.Main
for your ViewModel
. This means that you will by default execute all coroutines on the main thread. You should think about whether that's really something that you want to do. After all, very few non-UI things in Android need to be done on the main thread, and your ViewModel shouldn't be manipulating the UI directly.
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