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()
}
}
Kotlin coroutines use dispatchers to determine which threads are used for coroutine execution. To run code outside of the main thread, you can tell Kotlin coroutines to perform work on either the Default or IO dispatcher. In Kotlin, all coroutines must run in a dispatcher, even when they're running on the main thread.
annotation class ExperimentalCoroutinesApi. Content copied to clipboard. Marks declarations that are still experimental in coroutines API, which means that the design of the corresponding declarations has open issues which may (or may not) lead to their changes in the future.
runBlockingTest takes in a block of code and blocks the test thread until all of the coroutines it starts are finished. It also runs the code in the coroutines immediately (skipping any calls to delay ) and in the order they are called–-in short, it runs them in a deterministic order.
According to Google:
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:
Dispatchers.setMain
(like you are doing)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)
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
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