Test MutableSharedFlow with runTest - android

I’m having some trouble to test some state changes in my viewModel using MutableSharedFlow. For example, I have this class
class SampleViewModel : ViewModel() {
private val interactions = Channel<Any>()
private val state = MutableSharedFlow<Any>(
onBufferOverflow = BufferOverflow.DROP_LATEST,
extraBufferCapacity = 1
)
init {
viewModelScope.launch {
interactions.receiveAsFlow().collect { action ->
processAction(action)
}
}
}
fun flow() = state.asSharedFlow()
fun handleActions(action: Any) {
viewModelScope.launch {
interactions.send(action)
}
}
suspend fun processAction(action: Any) {
state.emit("Show loading state")
// process something...
state.emit("Show success state")
}
}
I'm trying to test it using this method (already using version 1.6.1):
#Test
fun testHandleActions() = runTest {
val emissions = mutableListOf<Any>()
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.flow().collect {
emissions.add(it)
}
}
viewModel.handleActions("")
assertTrue(emissions[0] is String)
assertTrue(emissions[1] is String)
job.cancel()
}
If I test the viewmodel.processAction() it works like a charm. But if I try to test handleActions() I only receive the first emission and then it throws an IndexOutOfBoundsException
If I use the deprecated runBlockingTest, it works
#Test
fun testHandleActionsWithRunBlockingTest() = runBlockingTest {
val emissions = mutableListOf<Any>()
val job = launch {
viewModel.flow().toList(emissions)
}
viewModel.handleActions("")
job.cancel()
assertTrue(emissions[0] is String)
assertTrue(emissions[1] is String)
}
Did I miss something using runTest block?

Related

After waiting for 60000 ms, the test coroutine is not completing - Android Test

I'm trying to test my ViewModel using StateFlow, and I'm having trouble writing a simple error scenario test code. I'm using sealed class to manager the UI Events and UI states, and I'm writing the first unit test for my viewmodel to the Register flow.
ViewModel.kt
#HiltViewModel
class RegisterViewModel #Inject constructor(
private val signupWithEmailAndPasswordUseCase: SignupWithEmailAndPasswordUseCase
) : ViewModel() {
private val registerStateFlow = MutableStateFlow<RegisterState>(RegisterState.Initial)
fun getRegisterStateFlow(): StateFlow<RegisterState> = registerStateFlow
fun onEvent(event: RegisterEvent) {
when (event) {
is RegisterEvent.RegisterNewUser -> {
viewModelScope.launch {
doRegister(event.data)
}
}
}
}
private fun doRegister(user: User) {
//TODO validate all fields
viewModelScope.launch(Dispatchers.IO) {
signupWithEmailAndPasswordUseCase(user).collectLatest {
when (it) {
is NetworkResult.Progress -> registerStateFlow.value = RegisterState.Loading
is NetworkResult.Failure -> registerStateFlow.value = RegisterState.NetworkError
is NetworkResult.Success -> registerStateFlow.value =
RegisterState.Success(it.data)
}
}
}
}
}
My test class
#OptIn(ExperimentalCoroutinesApi::class)
class RegisterViewModelTest{
private val useCase: SignupWithEmailAndPasswordUseCase = mockk(relaxed = true)
private val user = mockk<User>()
private val testDispatcher = StandardTestDispatcher()
#get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
#Before
fun setup(){
Dispatchers.setMain(testDispatcher)
clearAllMocks()
}
#After
fun tearDown(){
Dispatchers.resetMain()
}
#Test
fun `Error state should be shown while register is being loaded`() = runTest{
val viewmodel = RegisterViewModel(useCase)
coEvery { useCase.invoke(user) } returns flow {
emit(NetworkResult.Failure(IOException()))
}
viewmodel.onEvent(RegisterEvent.RegisterNewUser(user))
viewmodel.getRegisterStateFlow().collectIndexed { index, value ->
if (index == 0) assertEquals(RegisterState.Initial, value)
if (index == 1) assertEquals(RegisterState.NetworkError, value)
}
}
}
After a long time runing I receive this error -> After waiting for 60000 ms, the test coroutine is not completing
And I don't know how to solve these problem

Unit testing viewModel that uses StateFlow and Coroutines

Kotlin 1.4.21
I have a very simple ViewModel that uses coroutine and stateFlow. However, the unit test will fail as the stateFlow doesn't seem to get updated.
I think its because the test will finish before the stateFlow is updated.
expected not to be empty
This is my ViewModel under test
class TrendingSearchViewModel #Inject constructor(
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase,
private val coroutineDispatcher: CoroutineDispatcherProvider
) : ViewModel() {
private val trendingSearchMutableStateFlow = MutableStateFlow<List<String>>(emptyList())
val trendingSearchStateFlow = trendingSearchMutableStateFlow.asStateFlow()
fun getTrendingSearch() {
viewModelScope.launch(coroutineDispatcher.io()) {
try {
trendingSearchMutableStateFlow.value = loadTrendingSearchUseCase.execute()
} catch (exception: Exception) {
Timber.e(exception, "trending ${exception.localizedMessage}")
}
}
}
}
This is my actual test class, I have tried different things to get it to work
class TrendingSearchViewModelTest {
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase = mock()
private val coroutineDispatcherProvider = CoroutineDispatcherProviderImp()
private lateinit var trendingSearchViewModel: TrendingSearchViewModel
#Before
fun setUp() {
trendingSearchViewModel = TrendingSearchViewModel(
loadTrendingSearchUseCase,
coroutineDispatcherProvider
)
}
#Test
fun `should get trending search suggestions`() {
runBlocking {
// Arrange
val trending1 = UUID.randomUUID().toString()
val trending2 = UUID.randomUUID().toString()
val trending3 = UUID.randomUUID().toString()
whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOf(trending1, trending2, trending3))
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.value
}
// Act
trendingSearchViewModel.getTrendingSearch()
// Assert
val result = trendingSearchViewModel.trendingSearchStateFlow.value
assertThat(result).isNotEmpty()
job.cancel()
}
}
}
This is the usecase I am mocking in the test:
class LoadTrendingSearchUseCaseImp #Inject constructor(
private val searchCriteriaProvider: SearchCriteriaProvider,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider
) : LoadTrendingSearchUseCase {
override suspend fun execute(): List<String> {
return withContext(coroutineDispatcherProvider.io()) {
searchCriteriaProvider.provideTrendingSearch().trendingSearches
}
}
}
Just in case its needed this is my interface:
interface CoroutineDispatcherProvider {
fun io(): CoroutineDispatcher = Dispatchers.IO
fun default(): CoroutineDispatcher = Dispatchers.Default
fun main(): CoroutineDispatcher = Dispatchers.Main
fun immediate(): CoroutineDispatcher = Dispatchers.Main.immediate
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
}
class CoroutineDispatcherProviderImp #Inject constructor() : CoroutineDispatcherProvider
I think this library https://github.com/cashapp/turbine by Jack Wharton will be of great help in the future when you need more complex scenarios.
What I think is happening is that in fragment you are calling .collect { } and that is ensuring the flow is started. Check the Terminal operator definition: Terminal operators on flows are suspending functions that start a collection of the flow. https://kotlinlang.org/docs/flow.html#terminal-flow-operators
This is not true for sharedFlow, which might be configured to be started eagerly.
So to solve your issue, you might just call
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.collect()
}
This is what worked for me:
#Test
fun `should get trending search suggestions`() {
runBlockingTest {
// Arrange
val trending1 = UUID.randomUUID().toString()
val trending2 = UUID.randomUUID().toString()
val trending3 = UUID.randomUUID().toString()
val listOfTrending = listOf(trending1, trending2, trending3)
whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOfTrending)
/* List to collect the results */
val listOfEmittedResult = mutableListOf<List<String>>()
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.toList(listOfEmittedResult)
}
// Act
trendingSearchViewModel.getTrendingSearch()
// Assert
assertThat(listOfEmittedResult).isNotEmpty()
verify(loadTrendingSearchUseCase).execute()
job.cancel()
}
}

What is the substitute for runBlocking Coroutines in fragments and activities?

It is recommended to not use GlobalScope and runBlocking.
I have implemented changes in order to this topic:
End flow/coroutines task before go further null issue
However it doesn't work well as previously with runBlocking. In brief icon doesn't change, data is not on time.
My case is to change icon depending on the boolean.
usecase with Flow
class GetNotificationListItemDetailsUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseFlowUseCase<Unit, List<NotificationItemsResponse.NotificationItemData>>() {
override fun create(params: Unit): Flow<List<NotificationItemsResponse.NotificationItemData>> {
return flow{
emit(notificationDao.readAllData())
}
}
}
viewmodel
val actualNotificationList: Flow<List<NotificationItemsResponse.NotificationItemData>> = getNotificationListItemDetailsUseCase.build(Unit)
fragment
private fun getActualNotificationList() : Boolean {
lifecycleScope.launch {
vm.actualNotificationList
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
notificationData.value = response
val notificationDataString = notificationData.value.toString()
val stringToCheck = "isRead=false"
isNotificationNotRead = (notificationDataString.contains(stringToCheck))
}
}
return isNotificationNotRead
}
on method onViewCreated I have initToolbar to check if it's true and make action, with runBlokcing worked.
fun initToolbar{
if (onReceived) {
Log.d("onReceivedGoes", "GOES IF")
} else {
Log.d("onReceivedGoes", "GOES ELSE")
getActualNotificationList()
}
onReceived = false
val item = menu.findItem(R.id.action_notification_list)
when {
isNotificationNotRead && !isOutcomed -> {
item.setIcon(R.drawable.image_icon_change)
}
}
coroutine job before change, it worked well
val job = GlobalScope.launch {
vm.getNotificationListItemDetailsUseCase.build(Unit).collect {
notificationData.value = it
val notificationDataString = notificationData.value.toString()
val stringToCheck = "isRead=false"
isNotificationNotRead = (notificationDataString.contains(stringToCheck))
}
}
runBlocking {
job.join()
}
}
Another question is I have the same thing to do in MainActivity, but I do not use there a flow just suspend function.
UseCase
class UpdateNotificationListItemUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseUpdateBooleanUseCase<Int, Boolean, Boolean, Boolean, Unit>() {
override suspend fun create(itemId: Int, isRead: Boolean, isArchived: Boolean, isAccepted: Boolean){
notificationDao.updateBooleans(itemId, isRead, isArchived, isAccepted)
}
}
MainActivity
val job = GlobalScope.launch { vm.getIdWithUpdate() }
runBlocking {
job.join()
}
MainViewmodel
suspend fun getIdWithUpdate() {
var id = ""
id = notificationAppSessionStorage.getString(
notificationAppSessionStorage.getIncomingKeyValueStorage(),
""
)
if (id != "") {
updateNotificationListItemUseCase.build(id.toInt(), true, false, false)
}
}
}
EDIT1:
collect in fragments works perfectly, thanks
What about MainActivity and using this usecase with suspend fun without flow.
I have read documentation https://developer.android.com/kotlin/coroutines/coroutines-best-practices
val IODispatcher: CoroutineDispatcher = Dispatchers.IO
val externalScope: CoroutineScope = CoroutineScope(IODispatcher)
suspend {
externalScope.launch(IODispatcher) {
vm.getIdWithUpdate()
}.join()
}
Second option, but here I do not wait until job is done
suspend {
withContext(Dispatchers.IO) {
vm.getIdWithUpdate()
}
}
What do you think about it?
You can try to update the icon in the collect block:
private fun getActualNotificationList() = lifecycleScope.launch {
vm.actualNotificationList
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
notificationData.value = response
val notificationDataString = notificationData.value.toString()
val stringToCheck = "isRead=false"
val isNotificationNotRead = (notificationDataString.contains(stringToCheck))
val item = menu.findItem(R.id.action_notification_list)
when {
isNotificationNotRead && !isOutcomed -> {
item.setIcon(R.drawable.image_icon_change)
}
}
}
}
Using runBlocking you are blocking the Main Thread, which may cause an ANR.

Solution to pause and resume in RxJava similar to TestCoroutineScope

Full source code is available at : https://github.com/AliRezaeiii/StarWarsSearch-RxPaging
Here is my local unit test where I test a ViewModel while I am using Coroutines for networking :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
testCoroutineRule.runBlockingTest {
`when`(api.fetchShowList()).thenReturn(emptyList())
}
val repository = ShowRepository(dao, api, context, TestContextProvider())
testCoroutineRule.pauseDispatcher()
val viewModel = MainViewModel(repository)
assertThat(viewModel.shows.value, `is`(Resource.loading()))
testCoroutineRule.resumeDispatcher()
assertThat(viewModel.shows.value, `is`(Resource.success(emptyList())))
}
As you know I can pause and resume using TestCoroutineScope, so I can test when liveData is in Loading or Success state.
I wonder if we can do the same thing when we test while we are using RxJava.
At the moment I just can verify Success state :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(repository.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(repository.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(repository.getFilm(anyString())).thenReturn(Single.just(film))
viewModel = DetailViewModel(schedulerProvider, character,
GetSpecieUseCase(repository), GetFilmUseCase(repository))
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isNotEmpty())
assertTrue(data.species.isNotEmpty())
}
}
}
}
in ViewModel init block, I send the network request. You can review it in the bellow class. That can be tested using pause and resume while using Coroutines. How about RxJava?
open class BaseViewModel<T>(
private val schedulerProvider: BaseSchedulerProvider,
private val singleRequest: Single<T>
) : ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val _liveData = MutableLiveData<Resource<T>>()
val liveData: LiveData<Resource<T>>
get() = _liveData
init {
sendRequest()
}
fun sendRequest() {
_liveData.value = Resource.Loading
singleRequest.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui()).subscribe({
_liveData.postValue(Resource.Success(it))
}) {
_liveData.postValue(Resource.Error(it.localizedMessage))
Timber.e(it)
}.also { compositeDisposable.add(it) }
}
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
}
Without seeing what you tried, I can only guess there were two possible issues that required fixing:
Use the same TestScheduler for all provider methods:
class ImmediateSchedulerProvider : BaseSchedulerProvider {
val testScheduler = TestScheduler()
override fun computation(): Scheduler = testScheduler
override fun io(): Scheduler = testScheduler
override fun ui(): Scheduler = testScheduler
}
The unit tests weren't failing for the wrong state so they appear to pass even when the code hasn't run:
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
`when`(repository.getSpecie(anyString())).thenReturn(Single.just(specie))
`when`(repository.getPlanet(anyString())).thenReturn(Single.just(planet))
`when`(repository.getFilm(anyString())).thenReturn(Single.just(film))
viewModel = DetailViewModel(schedulerProvider, character, GetSpecieUseCase(repository),
GetPlanetUseCase(repository), GetFilmUseCase(repository))
viewModel.liveData.value.let {
assertThat(it, `is`(Resource.Loading))
}
schedulerProvider.testScheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS) // <-------------
viewModel.liveData.value.let {
assertThat(it, `is`(notNullValue()))
if (it is Resource.Success) {
it.data?.let { data ->
assertTrue(data.films.isNotEmpty())
assertTrue(data.species.isNotEmpty())
}
} else {
fail("Wrong type " + it) // <---------------------------------------------
}
}
}

suspendCancellableCoroutine returns CompletedWithCancellation instead of the actual type

I ran into a weird issue that manifested itself when I updated the kotlinx-coroutines-core dependency from 1.3.2 to 1.3.3. However, the self-contained example below reproduces the issue with 1.3.2 as well.
I have an extension method for a callback-based operation queue. This extension method uses suspendCancellableCoroutine to wrap the callback usage and to convert it to a suspend function. Now, it all works otherwise, but the resulting object that is returned from the suspending function is not of type T, but CompletedWithCancellation<T>, which is a private class of the coroutine library.
The weird thing is, if I call c.resume("Foobar" as T, {}) inside the suspendCancellableCoroutine, it works just fine. When using the callback routine, the value is a String before passing to to c.resume(), but it gets wrapped in a CompletedWithCancellation object.
Here's the code that reproduces the issue:
#ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {
#SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.plant(Timber.DebugTree())
setContentView(R.layout.activity_main)
val vm = ViewModelProviders.of(this)
.get(MainViewModel::class.java)
vm.liveData.observe(this, Observer {
findViewById<TextView>(R.id.mainText).text = "Got result: $it"
})
vm.getFoo()
}
}
#ExperimentalCoroutinesApi
class MainViewModel : ViewModel() {
private val manager = OperationManager()
val liveData = MutableLiveData<String>()
fun getFoo() {
viewModelScope.launch {
val op = Operation(manager, "Foobar")
val rawResult = op.get<Any>()
Timber.d("Raw result: $rawResult")
val op2 = Operation(manager, "Foobar")
val result = op2.get<String>()
Timber.d("Casted result: $result")
liveData.postValue(result)
}
}
}
class OperationManager {
private val operationQueue = ConcurrentLinkedQueue<Operation>()
private val handler = Handler(Looper.getMainLooper())
private val operationRunnable = Runnable { startOperations() }
private fun startOperations() {
val iter = operationQueue.iterator()
while (iter.hasNext()) {
val operation = iter.next()
operationQueue.remove(operation)
Timber.d("Executing operation $operation")
operation.onSuccess(operation.response)
}
}
fun run(operation: Operation) {
addToQueue(operation)
startDelayed()
}
private fun addToQueue(operation: Operation) {
operationQueue.add(operation)
}
private fun startDelayed() {
handler.removeCallbacks(operationRunnable)
handler.post(operationRunnable)
}
}
open class Operation(private val manager: OperationManager, val response: Any) {
private val listeners = mutableListOf<OperationListener>()
fun addListener(listener: OperationListener) {
listeners.add(listener)
}
fun execute() = manager.run(this)
fun onSuccess(data: Any) = listeners.forEach { it.onResult(data) }
}
#ExperimentalCoroutinesApi
suspend fun <T> Operation.get(): T = suspendCancellableCoroutine { c ->
#Suppress("UNCHECKED_CAST")
val callback = object : OperationListener {
override fun onResult(result: Any) {
Timber.d("get().onResult() -> $result")
c.resume(result as T, {})
}
}
addListener(callback)
execute()
}
interface OperationListener {
fun onResult(result: Any)
}
Do note that just before calling c.resume(), the type of result is String, as it should be. However, it's not String in getFoo() once the suspend function completes. What causes this?
The solution was in this:
c.resume(result as T)
Instead of:
c.resume(result as T, {})
It seems that the former handles the execution of resume() correctly after getResult() is called, whereas the latter only works if resume() is called before getResult().

Categories

Resources