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.
Related
I am writing some mess around code to try and test coroutines and flows in Android. Following a common pattern I wrote a coroutine rule for handling the dispatcher and setting the main thread:
class CoroutineScopeRule(
val dispatcher: TestDispatcher = StandardTestDispatcher(),
): TestWatcher() {
var dispatcherProvider: CoroutineDispatcherProvider
var sharingStrategyProvider: SharingStrategyProvider
init {
dispatcherProvider = CoroutineDispatcherProvider(
main = dispatcher,
default = dispatcher,
io = dispatcher
)
sharingStrategyProvider = SharingStrategyProvider(
lazily = SharingStarted.Lazily,
eagerly = SharingStarted.Lazily,
whileSubscribed = SharingStarted.Lazily
)
}
override fun starting(description: Description) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
super.finished(description)
Dispatchers.resetMain()
}
}
Now the problem is when I try inheriting a class that sets up this rule vs delegation one.
For the inheritance I have a simple base class:
open class BaseTest {
#get:Rule
val coroutineScopeRule = CoroutineScopeRule()
}
with the test being:
#OptIn(ExperimentalCoroutinesApi::class)
class MainViewModelTest : BaseTest() {
Then with the delegation pattern:
class CoroutineTestImpl : CoroutineTest {
#get:Rule
override val coroutineScopeRule = CoroutineScopeRule()
}
and test:
class MainViewModelTest : CoroutineTest by CoroutineTestImpl() {
When using the delegation pattern my tests fail and hang. From what I can tell it is due to the two providers that I have appearing to not be initializing properly. If I provide the dispatchers directly in my ViewModel impl then the tests pass with delegation. Wondering if others have run into this problem before and know why these components aren't being initialized.
Running coroutines version: 1.6.4
I have ViewModel which exposes flow to fragment. I am calling API from ViewModel's init which emits different states. I am not able to write unit test to check all the emitted states.
My ViewModel
class FooViewModel constructor( fooProvider : FooProvider){
private val _uiState = MutableSharedFlow<UiState>(replay = 1)
// Used in fragment to collect ui states.
val uiState = _uiState.asSharedFlow()
init{
_uiState.emit(FetchingFoo)
viewModelScope.runCatching {
// Fetch shareable link from server [users.sharedInvites.list].
fooProvider.fetchFoo().await()
}.fold(
onSuccess = {
_uiState.emit(FoundFoo)
},
onFailure = {
_uiState.emit(EmptyFoo)
}
)
}
sealed class UiState {
object FetchingFoo : UiState()
object FoundFoo : UiState()
object EmptyFoo : UiState()
}
}
Now I want to test this ViewModel to check if all the states are being emitted.
My Test: Note I am using turbine library.
class FooViewModelTest{
#Mock
private lateinit var fooProvider : FooProvider
#Test
fun testFooFetch() = runTest {
whenever(fooProvider.fetchFoo()).thenReturn(// Expected API response)
val fooViewModel = FooViewModel(fooProvider)
// Here lies the problem. as we create fooViewModel object API is called.
// before reaching test block.
fooViewModel.uiState.test{
// This condition fails as fooViewModel.uiState is now at FoundFoo.
assertEquals(FetchingFoo, awaitItem())
assertEquals(FoundFoo, awaitItem())
}
}
}
How to delay init till inside on .test{} block.
Tried creating ViewModel object by Lazy{} but not working.
It is not very pragmatic to "delay" emissions for sake of testing, this may produce flakey tests.
This is more of a coding issue - the right question should be "Does this logic belong in the class initialisation. The fact that it is more difficult to test should give you hints that it is less than ideal.
A better solution would be to use a StateFlow which is lazily initialised something like (some code assumed for sake of testing) :
class FooViewModel constructor(private val fooProvider: FooProvider) : ViewModel() {
val uiState: StateFlow<UiState> by lazy {
flow<UiState> {
emit(FoundFoo(fooProvider.fetchFoo()))
}.catch { emit(EmptyFoo) }
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5_000),
initialValue = FetchingFoo)
}
sealed class UiState {
object FetchingFoo : UiState()
data class FoundFoo(val list: List<Any>) : UiState()
object EmptyFoo : UiState()
}
}
fun interface FooProvider {
suspend fun fetchFoo(): List<Any>
}
Then testing could be something like :
class FooViewModelTest {
#ExperimentalCoroutinesApi
#Test fun `whenObservingUiState thenCorrectStatesObserved`() = runTest {
val states = mutableListOf<UiState>()
FooViewModel { emptyList() }
.uiState
.take(2)
.toList(states)
assertEquals(2, states.size)
assertEquals(listOf(FetchingFoo, FoundFoo(emptyList()), states)
}
#ExperimentalCoroutinesApi
#Test fun `whenObservingUiStateAndErrorOccurs thenCorrectStatesObserved`() = runTest {
val states = mutableListOf<UiState>()
FooViewModel { throw IllegalStateException() }
.uiState
.take(2)
.toList(states)
assertEquals(2, states.size)
assertEquals(listOf(FetchingFoo, EmptFoo), states)
}
}
addotional test dependencies :
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
testImplementation "android.arch.core:core-testing:1.1.1"
And then I'm seeing
[Test worker #coroutine#1] test
[Test worker #coroutine#2] viewModel
So this is giving me problems at the time to verify because it says that there's empty.
I'm using in my viewModel a CoroutineDispatcher injected with Hilt as
#HiltViewModel
class LocationsViewModel #Inject constructor(
private val locationsUseCase: LocationsUseCase,
#IODispatcher private val dispatcher: CoroutineDispatcher) : ViewModel() {
init { viewModelScope.launch(dispatcher) { locationsUseCase() }}
}
And the test I'm doing
private val testDispatcher = StandardTestDispatcher()
#Test
fun test() = runTest(testDispatcher){ ... }
fun createLocationsViewModel() = LocationsViewModel(locationsUseCase, testDispatcher)
Your viewmodel tries to initialise, but fails, because it doesn't know what to do with locationsUseCase call. Try mock it in setUp() method. With mockito-kotlin and mockito-core dependencies it would be something like
#Before
fun setUp() = runTest(testDispatcher) {
whenever(locationsUseCase.invoke()).thenReturn(Result.Success)
}
This code is not strict, because I don't know the rest of your code base. So think of it as a reference.
I'm trying to run a unit test on my RecyclerView. For my first test, I want to see if the RecyclerView is displayed.
#RunWith(RobolectricTestRunner::class)
class WordListFragmentTest {
// Executes task sin the Architecture component in the same thread.
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var scenario: FragmentScenario<WordListFragment>
private lateinit var viewModel: MainViewModel
val word = Word("Word")
#Before
fun setup() {
viewModel = mock(MainViewModel::class.java)
scenario = launchFragment(
factory = MainFragmentFactory(viewModel),
fragmentArgs = null,
themeResId = R.style.Theme_Words,
initialState = Lifecycle.State.RESUMED
)
}
#Test
fun `recyclerView displayed`() {
onView(withId(R.id.recyclerView))
.check(matches(isDisplayed()))
}
After running the test I get the following error.
java.lang.Exception: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(getMainLooper()).idle() call.
This appears to be related to LiveData observer that submits the list in the fragment. If I comment out the submit function the test will run.
The Fragment.
class WordListFragment(private val viewModel: MainViewModel) : Fragment() {
...
private fun submitList() {
viewModel.wordList.observe(viewLifecycleOwner, {
it?.let {
rvAdapter.submitList(it)
}
})
}
}
MianViewModel
class MainViewModel #Inject constructor(
var repository: IWordRepository,
#IoDispatcher var ioDispatcher: CoroutineDispatcher
) : ViewModel() {
var wordList: LiveData<List<Word>> = repository.allWords
...
}
This link states Robolectric will default to LooperMode.LEGACY behavior, but this can be overridden by applying a #LooperMode(NewMode) annotation to a test package, test class, or test method, or via the 'robolectric.looperMode' system property. I'm still experiencing the same error when I run my test.
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