These two methods invoke the same use-case. In the first version, I hard-coded Dispatchers.IO, and things work as expected.
The second version (which I prefer) uses an injected dispatcher that defaults to the Dispatchers.IO type. It fails with the IllegalStateException described in the comments. Any ideas?
#HiltViewModel
class MainViewModel #Inject constructor(
private val getUsers: GetUsers,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ViewModel() {
val liveData: MutableLiveData<List<User>> = MutableLiveData()
suspend fun getUsersByParamDispatcher(params: GetUsers.Params) {
// Successfully works as intended.
viewModelScope.launch(Dispatchers.IO) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
suspend fun getUsersByInjectDispatcher(params: GetUsers.Params) {
// IllegalStateException: Cannot access database on the main thread since it may potentially
// lock the UI for a long period of time.
// at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:494).
viewModelScope.launch(dispatcher) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
}
Logs confirm the exception and my curiosity is why are they different and how would I arrive at a working injected version.
Failing injected Dispatchers.IO:
>> coroutine.name: main
Working parameter Dispatchers.IO:
>> coroutine.name: DefaultDispatcher-worker-1
The dependencies are provided by #HiltViewModel and I expect dispatcher to respect its assigned default value. The Fragment creates this view model with the by viewModels() delegate.
It might be fine to hard-code the dispatcher. But with injection a blocking TestCoroutineDispatcher is easily passed during testing.
Maybe I'm overlooking something simple, or another way altogether.
// MainViewModelTest
#Before
fun setup() {
MockKAnnotations.init(this)
viewModel = MainViewModel(
getUsers,
coroutinesTestRule.testDispatcher
)
}
Related
I'm trying to do ViewModel testing using Kotlin(1.6.21) Coroutines(1.6.4) and Kotlin Flow.
Following official Kotlin coroutine testing documentation but ViewModel is not waiting/returning a result for suspending functions before test completion. Have gone through top StackOverflow answers and tried all suggested solutions like injecting the same CoroutineDispatcher, and passing the same CoroutineScope but none worked so far. So here I am posting the current simple test implementation. Have to post all classes code involved in the test case to get a better idea.
ReferEarnDetailViewModel.kt:
Injected Usecase and CoroutineContextProvider and calling API using viewModelScope with provided dispatcher. But after calling callReferEarnDetails() from the test case, it is not collecting any data emitted by the mock use case method. Have tried with the direct repo method call, without Kotlin flow as well but same failure.
#HiltViewModel class
ReferEarnDetailViewModel #Inject constructor(
val appDatabase: AppDatabase?,
private val referEarnDetailsUseCase: ReferEarnDetailsUseCase,
private val coroutineContextProvider: CoroutineContextProvider) : BaseViewModel() {
fun callReferEarnDetails() {
setProgress(true)
viewModelScope.launch(coroutineContextProvider.default + handler) {
referEarnDetailsUseCase.execute(UrlUtils.getUrl(R.string.url_referral_detail))
.collect { referEarnDetail ->
parseReferEarnDetail(referEarnDetail)
}
}
}
private fun parseReferEarnDetail(referEarnDetail:
ResultState<CommonEntity.CommonResponse<ReferEarnDetailDomain>>) {
when (referEarnDetail) {
is ResultState.Success -> {
setProgress(false)
.....
}
}
}
ReferEarnCodeUseCase.kt: Returning Flow of Api response.
#ViewModelScoped
class ReferEarnCodeUseCase #Inject constructor(private val repository:
IReferEarnRepository) :BaseUseCase {
suspend fun execute(url: String):
Flow<ResultState<CommonEntity.CommonResponse<ReferralCodeDomain>>> {
return repository.getReferralCode(url)
}
}
CoroutineTestRule.kt
#ExperimentalCoroutinesApi
class CoroutineTestRule(val testDispatcher: TestDispatcher =
StandardTestDispatcher()) : TestWatcher() {
val testCoroutineDispatcher = object : CoroutineContextProvider {
override val io: CoroutineDispatcher
get() = testDispatcher
override val default: CoroutineDispatcher
get() = testDispatcher
override val main: CoroutineDispatcher
get() = testDispatcher
}
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
ReferEarnDetailViewModelTest.kt
#RunWith(JUnit4::class)
#ExperimentalCoroutinesApi
class ReferEarnDetailViewModelTest {
private lateinit var referEarnDetailViewModel: ReferEarnDetailViewModel
private lateinit var referEarnDetailsUseCase: ReferEarnDetailsUseCase
#get:Rule
val coroutineTestRule = CoroutineTestRule()
#Mock
lateinit var referEarnRepository: IReferEarnRepository
#Mock
lateinit var appDatabase: AppDatabase
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
referEarnDetailsUseCase = ReferEarnDetailsUseCase(referEarnRepository)
referEarnDetailViewModel = ReferEarnDetailViewModel(appDatabase,
referEarnDetailsUseCase , coroutineTestRule.testCoroutineDispatcher)
}
#Test
fun `test api response parsing`() = runTest {
val data = ResultState.Success( TestResponse() )
//When
Mockito.`when`(referEarnDetailsUseCase.execute("")).thenReturn(flowOf(data))
//Call ViewModel function which further call usecase function.
referEarnDetailViewModel.callReferEarnDetails()
//This should be false after API success response but failing here....
assertEquals(referEarnDetailViewModel.showProgress.get(),false)
}
}
Have tried this solution:
How test a ViewModel function that launch a viewModelScope coroutine? Android
Kotlin
Inject and determine CoroutineScope on ViewModel creation
As it is stated in the documentation runTest awaits completion of all the launched in its TestScope coroutines (or throws a timeout). But it does so on exit from the test body. In your case assertEquals fails inside the test body, so test fails immediately.
Generally speaking, this mechanism of awaiting completion of all jobs is a mean of preventing leaks and is not suitable for your purpose.
There are two ways to control the coroutines execution inside the test body:
Use methods to control virtual time. E.g. advanceUntilIdle should help in this case - use it before asserting the result and it will execute all the tasks scheduled on the given TestDispatcher.
Use regular ways to await execution, e.g. return a job and await its' completion before checking the result. This requires some code redesign, but this is a recommended approach. Check out a couple of paragraphs above the Setting the Main dispatcher chapter.
In the Google Codelab for Coroutines, we are shown a MainCoroutineScopeRule. Within the rule, it explains that this rule can be extended to other dispatchers in addition to Dispatchers.Main:
override fun starting(description: Description?) {
super.starting(description)
// If your codebase allows the injection of other dispatchers like
// Dispatchers.Default and Dispatchers.IO, consider injecting all of them here
// and renaming this class to `CoroutineScopeRule`
//
// All injected dispatchers in a test should point to a single instance of
// TestCoroutineDispatcher.
Dispatchers.setMain(dispatcher)
}
My question is, how exactly are we to inject the other dispatchers? Does this assume that we're using dependency injection? If so, what if I'm not using DI, can I still extend this rule to the other dispatchers? I don't see anything in the kotlinx-coroutines-test library that allows me to set the TestCoroutineDispatcher to the other dispatchers. So, there's this:
Dispatchers.setMain(dispatcher)
...but not this:
Dispatchers.setIO(dispatcher) // Or Default, etc.
Am I instead expected to rewrite my suspend functions to take in a dispatcher as a parameter:
suspend doSomeIO(dispatcher: CoroutineDispatcher = Dispatchers.IO) {
launch(dispatcher) {
// Some long-running IO operation
}
}
You are correct in that this does assume you are injecting your dispatchers. If you are not using the main dispatcher, you should be injecting the dispatcher in order to test it properly.
The way you wrote the suspend function is one way to do it, if you want to force that particular function to be on the Dispatchers.IO thread. However, then you will end up having nested launches.
Instead of that, I would just pass the dispatcher in to a viewModel, and let the viewmodel decide how to call the suspend function.
//Your version:
suspend fun doSomeIO(dispatcher: CoroutineDispatcher = Dispatchers.IO) {
launch(dispatcher) {
// Some long-running IO operation
}
}
class MyViewModel(val dispatcher: CoroutineDispatcher = Dispatchers.IO: ViewModel() {
init {
viewModelScope.launch {
doSomeIO(dispatcher) // here you are launching one coroutine inside the other
}
}
}
// Instead try this:
suspend fun doSomeIO() {
// Some long-running IO operation
}
class MyViewModel(val dispatcher: CoroutineDispatcher = Dispatchers.IO: ViewModel() {
init {
viewModelScope.launch(dispatcher) {
doSomeIO()
}
}
}
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()
}
}
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:
Overrides the MainDispatcher via Dispatchers.setMain (like you are doing)
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)
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
When dealing with coroutines inside a viewModel is best to have said viewModel implement CoroutineScope so all coroutines are cancelled when the viewModel is cleared. Usually I see coroutineContext defined as Dispatchers.Main + _job so that coroutines are executed in the main UI thread by default. Usually this is done on a open class so that all your viewModels can extend it and get the scope without boilerplate code.
The issue arises when trying to unit test said viewModels as Dispatchers.Main is not available and trying to use it throws an exception. I am tryin to find a good solution that doesn't involve external libraries or too much boiler plate on the child viewModels.
My current solution is to add the maincontext as a contructor paramenter with the Dispatchers.Main as the default value. Then in the unit test, before testing the viewModel I set it to Dispatchers.Default. I don't quiet like this solution as it exposes the coroutineContext implementation details for everyone to see and change:
open class ScopedViewModel(var maincontext = Dispatchers.Main) : ViewModel(), CoroutineScope {
private val _job = Job()
override val coroutineContext: CoroutineContext
get() = maincontext + _job
override fun onCleared() {
super.onCleared()
_job.cancel()
}
}
class MyViewModel : ScopedViewModel() {}
In the tests:
fun setup(){
viewModel = MyViewModel()
viewModel.maincontext = Dispacther.Default
}
Personally I copied a solution from RxJava2: if your test runs against RxJava2 flow which includes two or more different schedulers, you want, sure, all of them to actually run in a single thread.
Here is how it is done with RxJava2 testing:
#BeforeClass
public static void prepare() {
RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setNewThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
RxJavaPlugins.setSingleSchedulerHandler(scheduler -> Schedulers.trampoline());
RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());
}
I did the same for coroutines. Just have created a class which collects dispatchers, but these dispatchers can be changed.
object ConfigurableDispatchers {
#JvmStatic
#Volatile
var Default: CoroutineDispatcher = Dispatchers.Default
#JvmStatic
#Volatile
var Main: MainCoroutineDispatcher = Dispatchers.Main
...
}
And, inside #BeforeClass method I call
#ExperimentalCoroutinesApi
fun setInstantMainDispatcher() {
Main = object : MainCoroutineDispatcher() {
#ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher
get() = this
override fun dispatch(context: CoroutineContext, block: Runnable) {
block.run()
}
}
}
That will guarantee that the block will be executed in the calling thread.
It is the only alternative I found to constructor injection.
I'm trying to write a unit test for a view model using live data.
LoginViewModel.kt
class LoginViewModel #Inject constructor(
val context: Context
): ViewModel() {
val username = MutableLiveData<String>()
val password = MutableLiveData<String>()
val isLoginButtonEnabled = MediatorLiveData<Boolean>().apply {
fun combineLatest(): Boolean {
return !(username.value.isNullOrEmpty() || password.value.isNullOrEmpty())
}
addSource(username) { this.value = combineLatest() }
addSource(password) { this.value = combineLatest() }
}
init {
username.postValue("test")
password.postValue("test")
}
}
LoginViewModelTest.kt
#RunWith(MockitoJUnitRunner::class)
class LoginViewModelTest {
#Rule
#JvmField
val instantTaskExecutorRole = InstantTaskExecutorRule()
private val context = mock(Context::class.java)
private val loginViewModel = LoginViewModel(context)
#Test
fun loginButtonDisabledOnEmptyUsername() {
val observer = mock<Observer<Boolean>>()
loginViewModel.isLoginButtonEnabled.observeForever(observer)
loginViewModel.username.postValue("")
verify(observer).onChanged(false)
}
}
My unit test throws the following exception at the line username.postValue("test"):
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
The InstantTaskExecutorRule should provide an execution context when using live data, however it doesn't work when initializing live data in the init-block. When omitting the init-block it works as desired, but i need the possibility to initialize live data variables.
Is there any way to make the live data initialization work when unit testing view models?
I managed to unit test my ViewModel that was using LiveData using mentioned rula - InstantTaskExecutorRule. But in my case the rule val declaration was a bit different:
#Suppress("unused")
#get:Rule
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
Edit:
#Before
#Throws(Exception::class)
fun prepare() {
MockitoAnnotations.initMocks(this)
}
Edit2:
For some weird reason I cannot reproduce this :)
Also, I think that the problem could be because of the way you're initializing your ViewModel -
private val loginViewModel = LoginViewModel(context)
I assume that it initializes too early, thus it's init block gets called too early too. Maybe it's reasonable to create it in the #Before method ? Like:
private lateinit var viewModel: LoginViewModel
#Before
#Throws(Exception::class)
fun prepare() {
loginViewModel = LoginViewModel(context)
}
I was seeing a similar issue when setting a LiveData value during the ViewModel's init. Demigod's solution pointed me in the right direction, but I wanted to explain a bit about what was going on and why in the lifecycle of the testing process.
When you have a ViewModel that sets the LiveData during init, it will be run as soon as the view model is initialized. When you initialize the view model in your unit test using val viewModel = MyViewModel(), that view model is instantiated at the same time as the test class is initialized. The problem there is any rules you may have are initialized at the same time, but are not actually run until after the class is completely initialized, so your ViewModel.init() is happening before the rules actually take effect. This means your live data isn't working on an instant executor, any Rx observables aren't being run on replaced schedulers, etc. So ultimately there are two ways of solving for this:
Define the view model as a lateinit var and initialize the view model as a in the #Before method of your test, which runs after rules are applied, or
Define the view model as a val viewModel by lazy { MyViewModel() }, which won't be run until you actually start calling it in your tests.
I prefer option 2 because it also allows me to set up any test-case-specific preconditions before my view model is ever initialized, and I don't have to do repetitive initialization code (which could be quite verbose) inside every test that requires it.
I had a similar issue and the answer provided by Demigod was not solving it. I finally found out where the devil was hiding so I share it here : my init block was set before the liveData initialization, which works fine when running the app, but not when running tests !
class MyViewModel : ViewModel() {
// init { // <-- Do not put the init block before the liveData
// _myLiveData.postValue("First")
// }
private val _myLiveData: MutableLiveData<String> = MutableLiveData()
val myLiveData: LiveData<String>
get() = _myLiveData
init {
_myLiveData.postValue("First")
}
}