Kotlin Mockito veirfy not invoked in a coroutine test - android

I got an error while doing a kotlin coroutine test. I'm pretty sure the function is called because I saw the logs. I'm not sure what does the error message mean. Is this related to coroutine (running on different thread) or the context (using a customized context to trigger onStart)?
Wanted but not invoked: testActivity.fetch();
-> at com.example.cws.testActivity.fetch(testActivity.kt:322)
However, there was exactly 1 interaction with this mock:
testActivity.getStateListener$TestApp_release();
-> at com.example.cws.testActivity.getStateListener$TestApp_release(testActivity.kt:113)
Implementation:
val stateListener = object : StateEventListener{
override fun onStart(testContext: TestEventContext) {
launch {
fetch()
}
}
}
suspend fun fetch() {
***
}
Test function:
#Test
fun should_fetch_when_started()= runBlocking {
testActivityController = Robolectric.buildActivity(TestActivity::class.java)
testActivity = spy(testActivityController
.create()
.start()
.get())
testActivity.stateListener.onStart()
verify(testActivity, times(1)).fetch()
}
I've tried to temporarily update the suspend function to a normal function, but same error observed.

I recommend trying the latest coroutine test library
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1"
It lets you wait for the coroutine to finish in the runTest scope (instead of runBlocking), like this:
#Test
fun should_fetch_when_started() = runTest {
testActivityController = Robolectric.buildActivity(TestActivity::class.java)
testActivity = spy(testActivityController
.create()
.start()
.get())
testActivity.stateListener.onStart()
advanceUntilIdle() // wait for the coroutine to complete
verify(testActivity, times(1)).fetch()
}
In your case, it probably ran the coroutine after the verify call instead of before.
You may need to define a Coroutine test rule as well to set the main dispatcher to a test dispatcher if you aren't already, like this:
#ExperimentalCoroutinesApi
class CoroutineTestRule constructor(val testDispatcher: TestDispatcher = StandardTestDispatcher())
: TestWatcher() {
val scope = TestScope(testDispatcher + Job())
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
and use it in your unit test
#OptIn(ExperimentalCoroutinesApi::class)
class ExampleUnitTest {
#get:Rule
var coroutineTestRule = CoroutineTestRule()
#Test
fun testCoroutineCall() = runTest {
var index = 0
coroutineTestRule.scope.launch {
delay(500)
index = 1
}
advanceUntilIdle()
assertEquals(1, index)
}
}

Related

How to test LiveData in ViewModel using coroutines

I am trying to test a function that executes a block of code launching a new coroutine, using this extension function:
fun ViewModel.execute(block: () -> Unit) = viewModelScope.launch(Dispatchers.IO) { block() }
Basically is launching a new coroutine in a ViewModel scope.
In my ViewModel, I use this function to invoke a use case and the result of this invocation, is a Kotlin Result, so I call the function fold to get the result and make the implementation for the success and the failure cases.
fun function() {
execute {
useCase.invoke()
).fold(
onSuccess = { },
onFailure = { }
)
}
}
In any of these cases, I am updating a LiveData object stored in my ViewModel.
I have tried to use the InstantExecutionRule
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
and I have also tried to set the coroutine context using
#get:Rule
val testCoroutineRule = TestCoroutineRule()
#ExperimentalCoroutinesApi
class TestCoroutineRule : TestRule {
val testCoroutineDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description) = object : Statement() {
#Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
}
}
fun runTest(block: suspend TestScope.() -> Unit) = testScope.runTest { block() }
}
But whenever I call this function from the test class, I get an error because the function inside the fold function (onSuccess and onFailure cases) is not called and the LiveData is not updated.
Do I miss something?
Thanks!

How to test a Loading Indicator with coroutines (TestCoroutineScope is deprecated)?

I am following this codelab but it seems to be outdated. TestCoroutineScope is deprecated.
#ExperimentalCoroutinesApi
class MainCoroutineRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()):
TestWatcher(),
TestCoroutineScope by TestCoroutineScope(dispatcher) {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
I've been trying to reproduce the following test
#Test
fun loadTasks_loading() {
// Pause dispatcher so you can verify initial values.
mainCoroutineRule.pauseDispatcher()
// Load the task in the view model.
statisticsViewModel.refresh()
// Then assert that the progress indicator is shown.
assertThat(statisticsViewModel.dataLoading.getOrAwaitValue(), `is`(true))
// Execute pending coroutines actions.
mainCoroutineRule.resumeDispatcher()
// Then assert that the progress indicator is hidden.
assertThat(statisticsViewModel.dataLoading.getOrAwaitValue(), `is`(false))
}
but I'm having no success. I don't know how to pause a dispatcher now.
How can I do that in the new way?
Found an answer here: Migrating to the new coroutines 1.6 test APIs by Marton Braun
#get:Rule
val mainDispatcherRule = MainDispatcherRule()
#Test
fun loadTasks_loading() = runTest {
// Main dispatcher will not run coroutines eagerly for this test
Dispatchers.setMain(StandardTestDispatcher())
// Load the task in the ViewModel
statisticsViewModel.refresh()
// Validate progress indicator is shown
assertThat(statisticsViewModel.dataLoading.getOrAwaitValue()).isTrue()
// Execute pending coroutine actions
// Wait until coroutine in statisticsViewModel.refresh() complete
advanceUntilIdle()
// Validate progress indicator is hidden
assertThat(statisticsViewModel.dataLoading.getOrAwaitValue()).isFalse()
}
MainDispatcherRule
#ExperimentalCoroutinesApi
class MainDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
If you hover over the deprecated TestCoroutineDispatcher you should see this pop up:
#Deprecated("The execution order of TestCoroutineDispatcher can be confusing, and the mechanism of pausing is typically misunderstood. Please use StandardTestDispatcher or UnconfinedTestDispatcher instead.", level = DeprecationLevel.WARNING)
So the solution here is to use StandardTestDispatcher or UnconfinedTestDispatcher

How to unit test view interface method called in Presenter with coroutines on Android?

I'm working on Android for a while but it's the first time I have to write some unit tests.
I have a design pattern in MVP so basically I have my Presenter, which have a contract (view) and it's full in kotlin, using coroutines.
Here is my Presenter class : The Repository and SomeOtherRepository are kotlin object so it's calling methods directly (The idea is to not change the way it's working actually)
class Presenter(private val contractView: ContractView) : CoroutinePresenter() {
fun someMethod(param1: Obj1, param2: Obj2) {
launch {
try {
withContext(Dispatchers.IO) {
val data = SomeService.getData() ?: run { throw Exception(ERROR) } // getData() is a suspend function
Repository.doRequest(param1, param2) // doRequest() is a suspend function also
}.let { data ->
if (data == null) {
contractView.onError(ERROR)
} else {
if (SomeOtherRepository.validate(data)) {
contractView.onSuccess()
} else {
contractView.onError(ERROR)
}
}
} catch (exception: Exception) {
contractView.onError(exception)
}
}
}
}
So the goal for me is to create unit test for this Presenter class so I created the following class in order to test the Presenter. Here is the Test implementation :
I read a lot of articles and stackoverflow links but still have a problem.
I setup a TestCoroutineRule which is like this :
#ExperimentalCoroutinesApi
class TestCoroutineRule(
private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher(), TestCoroutineScope by TestCoroutineScope() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
private fun TestCoroutineRule.runBlockingTest(block: suspend () -> Unit) =
testDispatcher.runBlockingTest { block() }
}
And here is the PresenterTest implementation :
#ExperimentalCoroutinesApi
class PresenterTest {
#get:Rule
val testCoroutineRule = TestCoroutineRule()
#Mock
private lateinit var view: ContractView
#Mock
private lateinit var repository: Repository
private lateinit var presenter: Presenter
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
presenter = Presenter(view)
}
#Test
fun `test success`() =
testCoroutineRule.runBlockingTest {
// Given
val data = DummyData("test", 0L)
// When
Mockito.`when`(repository.doRequest(param1, param2)).thenReturn(data)
// Then
presenter.someMethod("test", "test")
// Assert / Verify
Mockito.verify(view, Mockito.times(1)).onSuccess()
}
}
The problem I have is the following error Wanted but not invoked: view.onSuccess(); Actually there were zero interactions with this mock.
The ContractView is implemented in the Activity so I was wondering if I have to use Robolectric in order to trigger the onSuccess() method within the Activity context. I also think that I have a problem regarding the usage of coroutines maybe. I tried a lot of things but I always got this error on the onSuccess et onError view, if anyone could help, would be really appreciated :)
There could be other problems, but at a minimum you are missing:
Mockito.`when`(someOtherRepository.validate(data)).thenReturn(data)
Mockito.`when`(someService.getData()).thenReturn(data)
Use your debugger and check your logs to inspect what the test is doing

Coroutines - unit testing viewModelScope.launch methods

I am writing unit tests for my viewModel, but having trouble executing the tests. The runBlocking { ... } block doesn't actually wait for the code inside to finish, which is surprising to me.
The test fails because result is null. Why doesn't runBlocking { ... } run the launch block inside the ViewModel in blocking fashion?
I know if I convert it to a async method that returns a Deferred object, then I can get the object by calling await(), or I can return a Job and call join(). But, I'd like to do this by leaving my ViewModel methods as void functions, is there a way to do this?
// MyViewModel.kt
class MyViewModel(application: Application) : AndroidViewModel(application) {
val logic = Logic()
val myLiveData = MutableLiveData<Result>()
fun doSomething() {
viewModelScope.launch(MyDispatchers.Background) {
System.out.println("Calling work")
val result = logic.doWork()
System.out.println("Got result")
myLiveData.postValue(result)
System.out.println("Posted result")
}
}
private class Logic {
suspend fun doWork(): Result? {
return suspendCoroutine { cont ->
Network.getResultAsync(object : Callback<Result> {
override fun onSuccess(result: Result) {
cont.resume(result)
}
override fun onError(error: Throwable) {
cont.resumeWithException(error)
}
})
}
}
}
// MyViewModelTest.kt
#RunWith(RobolectricTestRunner::class)
class MyViewModelTest {
lateinit var viewModel: MyViewModel
#get:Rule
val rule: TestRule = InstantTaskExecutorRule()
#Before
fun init() {
viewModel = MyViewModel(ApplicationProvider.getApplicationContext())
}
#Test
fun testSomething() {
runBlocking {
System.out.println("Called doSomething")
viewModel.doSomething()
}
System.out.println("Getting result value")
val result = viewModel.myLiveData.value
System.out.println("Result value : $result")
assertNotNull(result) // Fails here
}
}
What you need to do is wrap your launching of a coroutine into a block with given dispatcher.
var ui: CoroutineDispatcher = Dispatchers.Main
var io: CoroutineDispatcher = Dispatchers.IO
var background: CoroutineDispatcher = Dispatchers.Default
fun ViewModel.uiJob(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(ui) {
block()
}
}
fun ViewModel.ioJob(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(io) {
block()
}
}
fun ViewModel.backgroundJob(block: suspend CoroutineScope.() -> Unit): Job {
return viewModelScope.launch(background) {
block()
}
}
Notice ui, io and background at the top. Everything here is top-level + extension functions.
Then in viewModel you start your coroutine like this:
uiJob {
when (val result = fetchRubyContributorsUseCase.execute()) {
// ... handle result of suspend fun execute() here
}
And in test you need to call this method in #Before block:
#ExperimentalCoroutinesApi
private fun unconfinifyTestScope() {
ui = Dispatchers.Unconfined
io = Dispatchers.Unconfined
background = Dispatchers.Unconfined
}
(Which is much nicer to add to some base class like BaseViewModelTest)
As others mentioned, runblocking just blocks the coroutines launched in it's scope, it's separate from your viewModelScope.
What you could do is to inject your MyDispatchers.Background and set the mainDispatcher to use dispatchers.unconfined.
As #Gergely Hegedus mentions above, the CoroutineScope needs to be injected into the ViewModel. Using this strategy, the CoroutineScope is passed as an argument with a default null value for production. For unit tests the TestCoroutineScope will be used.
SomeUtils.kt
/**
* Configure CoroutineScope injection for production and testing.
*
* #receiver ViewModel provides viewModelScope for production
* #param coroutineScope null for production, injects TestCoroutineScope for unit tests
* #return CoroutineScope to launch coroutines on
*/
fun ViewModel.getViewModelScope(coroutineScope: CoroutineScope?) =
if (coroutineScope == null) this.viewModelScope
else coroutineScope
SomeViewModel.kt
class FeedViewModel(
private val coroutineScopeProvider: CoroutineScope? = null,
private val repository: FeedRepository
) : ViewModel() {
private val coroutineScope = getViewModelScope(coroutineScopeProvider)
fun getSomeData() {
repository.getSomeDataRequest().onEach {
// Some code here.
}.launchIn(coroutineScope)
}
}
SomeTest.kt
#ExperimentalCoroutinesApi
class FeedTest : BeforeAllCallback, AfterAllCallback {
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val repository = mockkClass(FeedRepository::class)
private var loadNetworkIntent = MutableStateFlow<LoadNetworkIntent?>(null)
override fun beforeAll(context: ExtensionContext?) {
// Set Coroutine Dispatcher.
Dispatchers.setMain(testDispatcher)
}
override fun afterAll(context: ExtensionContext?) {
Dispatchers.resetMain()
// Reset Coroutine Dispatcher and Scope.
testDispatcher.cleanupTestCoroutines()
testScope.cleanupTestCoroutines()
}
#Test
fun topCafesPoc() = testDispatcher.runBlockingTest {
...
val viewModel = FeedViewModel(testScope, repository)
viewmodel.getSomeData()
...
}
}
I tried the top answer and worked, but I didn't want to go over all my launches and add a dispatcher reference to main or unconfined in my tests. So I ended up adding this code to my base testing class. I am defining my dispatcher as TestCoroutineDispatcher()
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
private val mainThreadDispatcher = TestCoroutineDispatcher()
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance()
.setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
Dispatchers.setMain(mainThreadDispatcher)
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
Dispatchers.resetMain()
}
}
in my base test class I have
#ExtendWith(MockitoExtension::class, InstantExecutorExtension::class)
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class BaseTest {
#BeforeAll
private fun doOnBeforeAll() {
MockitoAnnotations.initMocks(this)
}
}
I did use the mockk framework that helps to mock the viewModelScope instance like below
https://mockk.io/
viewModel = mockk<MyViewModel>(relaxed = true)
every { viewModel.viewModelScope}.returns(CoroutineScope(Dispatchers.Main))
There are 3 steps that you need to follow.
Add dependency in gradle file.
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1")
{ exclude ("org.jetbrains.kotlinx:kotlinx-coroutines-debug") }
Create a Rule class MainCoroutineRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
#ExperimentalCoroutinesApi
class MainCoroutineRule(private val testDispatcher: TestDispatcher = StandardTestDispatcher()) :
TestWatcher() {
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
Modify your test class to use ExperimentalCoroutinesApi runTest and advanceUntilIdle()
#OptIn(ExperimentalCoroutinesApi::class) // New addition
internal class ConnectionsViewModelTest {
#ExperimentalCoroutinesApi
#get:Rule
var mainCoroutineRule = MainCoroutineRule() // New addition
...
#Test
fun test_abcd() {
runTest { // New addition
...
val viewModel = MyViewModel()
viewModel.foo()
advanceUntilIdle() // New addition
verify { mockObject.footlooseFunction() }
}
}
For explanation on why to do this you can always refer to the codelab https://developer.android.com/codelabs/advanced-android-kotlin-training-testing-survey#3
The problem you are having stems not from runBlocking, but rather from LiveData not propagating a value without an attached observer.
I have seen many ways of dealing with this, but the simplest is to just use observeForever and a CountDownLatch.
#Test
fun testSomething() {
runBlocking {
viewModel.doSomething()
}
val latch = CountDownLatch(1)
var result: String? = null
viewModel.myLiveData.observeForever {
result = it
latch.countDown()
}
latch.await(2, TimeUnit.SECONDS)
assertNotNull(result)
}
This pattern is quite common and you are likely to see many projects with some variation of it as a function/method in some test utility class/file, e.g.
#Throws(InterruptedException::class)
fun <T> LiveData<T>.getTestValue(): T? {
var value: T? = null
val latch = CountDownLatch(1)
val observer = Observer<T> {
value = it
latch.countDown()
}
latch.await(2, TimeUnit.SECONDS)
observeForever(observer)
removeObserver(observer)
return value
}
Which you can call like this:
val result = viewModel.myLiveData.getTestValue()
Other projects make it a part of their assertions library.
Here is a library someone wrote dedicated to LiveData testing.
You may also want to look into the Kotlin Coroutine CodeLab
Or the following projects:
https://github.com/googlesamples/android-sunflower
https://github.com/googlesamples/android-architecture-components
You don't have to change the ViewModel's code, the only change is required to properly set coroutine scope (and dispatcher) when putting ViewModel under test.
Add this to your unit test:
#get:Rule
open val coroutineTestRule = CoroutineTestRule()
#Before
fun injectTestCoroutineScope() {
// Inject TestCoroutineScope (coroutineTestRule itself is a TestCoroutineScope)
// to be used as ViewModel.viewModelScope fro the following reasons:
// 1. Let test fail if coroutine launched in ViewModel.viewModelScope throws exception;
// 2. Be able to advance time in tests with DelayController.
viewModel.injectScope(coroutineTestRule)
}
CoroutineTestRule.kt
#Suppress("EXPERIMENTAL_API_USAGE")
class CoroutineTestRule : TestRule, TestCoroutineScope by TestCoroutineScope() {
val dispatcher = coroutineContext[ContinuationInterceptor] as TestCoroutineDispatcher
override fun apply(
base: Statement,
description: Description?
) = object : Statement() {
override fun evaluate() {
Dispatchers.setMain(dispatcher)
base.evaluate()
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
}
The code will be executed sequentially (your test code, then view model code, then launched coroutine) due to the replaced main dispatcher.
The advantages of the approach above:
Write test code as normal, no need to use runBlocking or so;
Whenever a crash happen in coroutine, that will fail the test (because of cleanupTestCoroutines() called after every test).
You can test coroutine which uses delay internally. For that test code should be run in coroutineTestRule.runBlockingTest { } and advanceTimeBy() be used to move to the future.

Coroutines unit tests pass individually but not when run together

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?
Test class
#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")))
}
}
ViewModel
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()
}
}
Repository
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 Jobs:
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.

Categories

Resources