21

So maybe there has been a tutorial going over this, but none of the ones I have read have addressed this issue for me. I have the structure as below and am trying to unit test, but when I go to test I always fails stating the repo method doSomthing() was never called. My best guess is because i have launched a new coroutine in a different context. How do I test this then?

Repository

interface Repository {
    suspend fun doSomething(): String
}

View Model

class ViewModel(val repo: Repository) {
    val liveData = MutableLiveData<String>()
    fun doSomething {
    //Do something here
        viewModelScope.launch(Dispatchers.IO) {
            val data = repo.doSomething()
            withContext(Dispatchers.Main) {
                liveData.value = data
            }
        }
    }
}

View Model Test

class ViewModelTest {
    lateinit var viewModel: ViewModel
    lateinit var repo: Repository

    @Before
    fun setup() {
        Dispatchers.setMain(TestCoroutineDispatcher())
        repo = mock<Repository>()
        viewModel = ViewModel(repo)
    }

    @Test
    fun doSomething() = runBlockingTest {
        viewModel.doSomething()
        viewModel.liveData.test().awaitValue().assertValue {
            // assert something
        }
        verify(repo).doSomthing()
    }
}
Adrian Le Roy Devezin
  • 672
  • 1
  • 13
  • 41
  • How is `viewModelScope` set up? If you expose it to the test, you could write `viewModelScope.coroutineContext[Job]!!.children.forEach { it.join() }`. However, this is still flaky because the test has no idea really how many child jobs you're going to launch and when they will be launched. You might get an empty `children` list. The way I handle a similar concern is by using an `assertTrueEventually` primitive that keeps retrying the assertion until it's satisfied or until a timeout elapses. – Marko Topolnik Jan 07 '20 at 09:38
  • Also, I have a general comment on your code: it would be better design to launch the coroutine in the Main dispatcher and have `suspend fun doSomething()` that internally uses the IO dispatcher as needed. This shouldn't be the concern of the caller. – Marko Topolnik Jan 07 '20 at 09:43
  • viewModelSCope is an extension function provided by Android which uses Dispatchers.Main... @MarkoTopolnik – Adrian Le Roy Devezin Jan 07 '20 at 15:25

2 Answers2

36

According to Google: enter image description here

Dispatchers should be injected into your ViewModels so you can properly test. You are setting the TestCorotutineDispatcher as the main Dispatcher via Dispatchers.setMain which takes control over the MainDispatcher, but you still have no control over the the execution of the coroutine launched via viewModelScope.launch(Dispatchers.IO).

Passing the Dispatcher via the constructor would make sure that your test and production code use the same dispatcher.

Typically an @Rule is defined that:

  1. Overrides the MainDispatcher via Dispatchers.setMain (like you are doing)
  2. Uses the TestCoroutineDispatcher's own runBlockingTest() to actually run the test.

Here is a really nice talk about testing and coroutines that happened at last year's Android Dev Summit.

And here is an example of such an @Rule. (Shameless plug. There are also examples of coroutine tests on that repo as well)

Emmanuel
  • 13,083
  • 4
  • 39
  • 53
  • That's great! It is possible also to use default Dispatcher by overloading the viewModel constructor - in Kotlin `class MyViewModel( private val dispatcher : CoroutineDispatcher = Dispatchers.IO)` That way I need to set the dispatcher only when testing. – hba Dec 19 '21 at 03:46
  • The Dispatchers.setMain does not seem to have an effect on Dispatchers.IO ? Or should it? – Rowan Gontier Feb 11 '23 at 08:40
2

I write this solution for who use Dagger.

Inject CoroutineDispatcher in ViewModel constructor like this:

class LoginViewModel @Inject constructor(val dispatcher: CoroutineDispatcher) : BaseViewModel() {

and Provide Dispatcher like this:

@Singleton
@Provides
fun provideDispatchers(): CoroutineDispatcher = Dispatchers.IO

and in test package, Provide Dispatcher like this:

@Singleton
@Provides
fun provideDispatchers(): CoroutineDispatcher = UnconfinedTestDispatcher()

and now all lines in viewModelScope.launch(dispatcher) will be run