Get livedata latest value while testing view model - testing

I'm trying to unit test my viewmodel:
private val loginRepository: LoginRepository = LoginRepository()
private val _loginSuccess = MutableLiveData<Resource<String>>()
val loginSuccess : LiveData<Resource<String>>
get() = _loginSuccess
fun login(credentials : RequestLogin){
_loginSuccess.value = Resource.loading()
viewModelScope.launch {
_loginSuccess.postValue(loginRepository.login(credentials))
}
With this:
#Test
fun login_success(){
val loginRequest = RequestLogin("username", "test")
val app:Application = ApplicationProvider.getApplicationContext()
PreferencesHelper.init(app)
val viewModel = LoginViewModel(app)
viewModel.loginSuccess.observeForever(dataObserver)
runBlocking {
viewModel.login(loginRequest)
assertEquals(viewModel.loginSuccess.getOrAwaitValue(), Resource.success("OK"))
}
viewModel.loginSuccess.removeObserver(dataObserver)
}
But everytime i'm getting just the first value of the liveData object Resource.loading() instead of the one obtained with the postValue method.
How can i ignore the result of the first liveData update and just get the final one?

runBlocking executes and waits for completion for the block you pass to it, in this case it is
viewModel.login(loginRequest)
assertEquals(viewModel.loginSuccess.getOrAwaitValue(), Resource.success("OK"))
But this code does not have any suspend calls, so runBlocking does not have any effect here. In particular it does not affect the viewModelScope.launch call.
There are a couple of ways to test this code. I would suggest using kotlinx-coroutines-test library. It provides TestCoroutineDispatcher which is very convenient in this case.
viewModelScope uses Dispatchers.Main dispatcher by default, so you need to replace it with TestCoroutineDispatcher. E.g. you can create a simple test rule:
class CoroutineTestRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
Then apply it to your test:
#get:Rule
var coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
And use it like this
#Test
fun login_success(){
...
viewModel.login(loginRequest)
coroutineTestRule.dispatcher.advanceUntilIdle()
assertEquals(viewModel.loginSuccess.getOrAwaitValue(), Resource.success("OK"))
...
}
And here is a bit of how it works:
CoroutineTestRule replaces Dispatcher.Main with the CoroutineTestRule.dispatcher
Your viewmodel launches a login job, using viewModelScope, which uses the same CoroutineTestRule.dispatcher
coroutineTestRule.dispatcher.advanceUntilIdle() makes the dispatcher to execute all outstanding tasks, so it will execute all coroutines, which are using this dispatcher and are ready to be executed.
There is also very convenient advanceTimeBy method on TestCoroutineDispatcher which allows you to fast-forward and skip e.g. delay calls.

Related

Do I need to warp the collectAsState() of a hot Flow with repeatOnLifecycle in #Composable?

I have read the article A safer way to collect flows from Android UIs.
I know the following content.
A cold flow backed by a channel or using operators with buffers such as buffer, conflate, flowOn, or shareIn is not safe to collect with some of the existing APIs such as CoroutineScope.launch, Flow.launchIn, or LifecycleCoroutineScope.launchWhenX, unless you manually cancel the Job that started the coroutine when the activity goes to the background. These APIs will keep the underlying flow producer active while emitting items into the buffer in the background, and thus wasting resources.
The Code A is from the official sample project.
The viewModel.suggestedDestinations is a MutableStateFlow, it's a hot Flow.
I don't know if the operation collectAsState() of hot Flow is safe in #Composable UI.
1: Do I need to use the Code just like Code B or Code C replace Code A for a hot Flow?
2: Is the operation collectAsState() of cold Flow safe in #Composable UI.
Code A
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun CraneHomeContent(
onExploreItemClicked: OnExploreItemClicked,
openDrawer: () -> Unit,
modifier: Modifier = Modifier,
viewModel: MainViewModel = viewModel(),
) {
val suggestedDestinations by viewModel.suggestedDestinations.collectAsState()
...
}
#HiltViewModel
class MainViewModel #Inject constructor(
...
) : ViewModel() {
...
private val _suggestedDestinations = MutableStateFlow<List<ExploreModel>>(emptyList())
val suggestedDestinations: StateFlow<List<ExploreModel>>
}
Code B
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
...
}
}
}
}
Code C
#Composable
fun LocationScreen(locationFlow: Flow<Flow>) {
val lifecycleOwner = LocalLifecycleOwner.current
val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
val location by locationFlowLifecycleAware.collectAsState()
...
}
collectAsState (Code A) is safe for any kind of Flow (cold/hot it doesn't matter). If you look at how collectAsState is implemented then you will see that it uses a LaunchedEffect deep down (collectAsState -> produceState -> LaunchedEffect)
internal class LaunchedEffectImpl(
parentCoroutineContext: CoroutineContext,
private val task: suspend CoroutineScope.() -> Unit
) : RememberObserver {
private val scope = CoroutineScope(parentCoroutineContext)
private var job: Job? = null
override fun onRemembered() {
job?.cancel("Old job was still running!")
job = scope.launch(block = task)
}
override fun onForgotten() {
job?.cancel()
job = null
}
override fun onAbandoned() {
job?.cancel()
job = null
}
}
which creates a coroutine scope and launches the task lambda once it enters the composition and cancels it automatically once it leaves the composition.
In Code A, viewModel.suggestedDestinations.collectAsState() (together with it's LaunchedEffect and it's coroutine scope) will be active as long as CraneHomeContent is being called by some other code. As soon as CraneHomeContent is stopped being called the LaunchedEffect inside of collectAsState() is canceled (and coroutine scope as well).
If it's called from multiple places then there will be multiple LaunchedEffects and thus multiple coroutine scopes.

Kotlin Coroutines for fetching from raw.githubusercontents.com

I am currently working on porting a Kotlin library to Kotlin/Js. I am currently struggling with how to fetch something from the web. I want to retrieve a page from raw.githubusercontent.com (example).
My code currently looks like this:
// function
fun getText(url: String): String? {
val url = "some url"
var text: String? = null
GlobalScope.launch {
text = window.fetch(url).await().body.toString()
}
return text
}
//test
#Test
fun run_test() = runTest {
val text = getText(url)
assertNotNull(text)
}
But I never seem to get the code in the Coroutine executed when running my unit tests. It's my first time working with coroutines, so I am not too familiar with them.
Have a nice day
By using the GlobalScope you've broke the structured concurrency. Try to avoid it.
Your window.fetch just runs in the topmost scope, so your test (runTest) doesn't wait for it's completion. It's like a daemon thread.
I would try to rewrite it to something like:
suspend fun getText(url: String): String? {
val url = "some url"
val text = window.fetch(url).await().body.toString()
return text
}
//test
#Test
fun run_test() = runTest {
val text = getText(url)
assertNotNull(text)
}
Well, if it's an interface, it means that you:
Cannot change the parameters of the method (i.e. pass the scope)
Cannot make it an extension function (i.e. on the scope)
Cannot add suspend modifier
…
the only solution you probably have is to use runBlocking:
fun getText(url: String): String? {
val url = "some url"
val text = runBlocking {
window.fetch(url).await().body.toString()
}
return text
}

Kotlin coroutines Flow is not canceled

I have a UseCase class with method that returns Flow
fun startTimeTicker(startTime: Long) = flow {
val countdownStart = System.currentTimeMillis()
for (currentTime in countdownStart..startTime step ONE_SECOND_MILLIS) {
.... irrelevant ...
emit("Some String")
delay(ONE_SECOND_MILLIS)
}
}
I'm collecting emmitted date in ViewModel like this
private fun startCollecting(startTime: Long) = launch {
matchStartTimerUseCase.startTimeTicker(startTime).collect {
_startTimeCountdown.postValue(it)
}
}
and my fragment is observing LiveData and displaying values. It works as expected until the time I leave the screen. As launch is called with coroutineScope of ViewModel shouldn't it get canceled and not emit values anymore?
Scope of ViewModel is implemented in BaseViewModel from which my ViewModel extends like this:
abstract class BaseViewModel(
private val dispatcherProvider: DispatcherProvider
) : ViewModel(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job + dispatcherProvider.provideUIContext()
private val job = SupervisorJob()
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
Am I forgetting to add some custom cancellation logic or missing something other?
If scope your ViewModel is Activity's life cycle ViewModelProvider(requireActivity()).get(YOUR_VIEWMODEL::class.java)then onCleared won't be called unless your Activity gets destroyed, but not with rotation changes.
First, make sure that onCleared() is called, if it's not called you can call it any life cycle method of Fragment or Activity.

Kotlin Flow: Testing hangs

I am trying to test Kotlin implementation using Flows. I use Kotest for testing. This code works:
ViewModel:
val detectedFlow = flow<String> {
emit("123")
delay(10L)
emit("123")
}
Test:
class ScanViewModelTest : StringSpec({
"when the flow contains values they are emitted" {
val detectedString = "123"
val vm = ScanViewModel()
launch {
vm.detectedFlow.collect {
it shouldBe detectedString
}
}
}
})
However, in the real ViewModel I need to add values to the flow, so I use ConflatedBroadcastChannel as follows:
private val _detectedValues = ConflatedBroadcastChannel<String>()
val detectedFlow = _detectedValues.asFlow()
suspend fun sendDetectedValue(detectedString: String) {
_detectedValues.send(detectedString)
}
Then in the test I try:
"when the flow contains values they are emitted" {
val detectedString = "123"
val vm = ScanViewModel()
runBlocking {
vm.sendDetectedValue(detectedString)
}
runBlocking {
vm.detectedFlow.collect { it shouldBe detectedString }
}
}
The test just hangs and never completes. I tried all kind of things: launch or runBlockingTest instead of runBlocking, putting sending and collecting in the same or separate coroutines, offer instead of send... Nothing seems to fix it. What am I doing wrong?
Update: If I create flow manually it works:
private val _detectedValues = ConflatedBroadcastChannel<String>()
val detectedFlow = flow {
this.emit(_detectedValues.openSubscription().receive())
}
So, is it a bug in asFlow() method?
The problem is that the collect function you used in your test is a suspend function that will suspend the execution until the Flow is finished.
In the first example, your detectedFlow is finite. It will just emit two values and finish. In your question update, you are also creating a finite flow, that will emit a single value and finish. That is why your test works.
However, in the second (real-life) example the flow is created from a ConflatedBroadcastChannel that is never closed. Therefore the collect function suspends the execution forever. To make the test work without blocking the thread forever, you need to make the flow finite too. I usually use the first() operator for this. Another option is to close the ConflatedBroadcastChannel but this usually means modifications to your code just because of the test which is not a good practice.
This is how your test would work with the first() operator
"when the flow contains values they are emitted" {
val detectedString = "123"
val vm = ScanViewModel()
runBlocking {
vm.sendDetectedValue(detectedString)
}
runBlocking {
vm.detectedFlow.first() shouldBe detectedString
}
}

How to suspend kotlin coroutine until notified

I would like to suspend a kotlin coroutine until a method is called from outside, just like the old Java object.wait() and object.notify() methods. How do I do that?
Here: Correctly implementing wait and notify in Kotlin is an answer how to implement this with Kotlin threads (blocking). And here: Suspend coroutine until condition is true is an answer how to do this with CompleteableDeferreds but I do not want to have to create a new instance of CompleteableDeferred every time.
I am doing this currently:
var nextIndex = 0
fun handleNext(): Boolean {
if (nextIndex < apps.size) {
//Do the actual work on apps[nextIndex]
nextIndex++
}
//only execute again if nextIndex is a valid index
return nextIndex < apps.size
}
handleNext()
// The returned function will be called multiple times, which I would like to replace with something like notify()
return ::handleNext
From: https://gitlab.com/SuperFreezZ/SuperFreezZ/blob/master/src/superfreeze/tool/android/backend/Freezer.kt#L69
Channels can be used for this (though they are more general):
When capacity is 0 – it creates RendezvousChannel. This channel does not have any buffer at all. An element is transferred from sender to receiver only when send and receive invocations meet in time (rendezvous), so send suspends until another coroutine invokes receive and receive suspends until another coroutine invokes send.
So create
val channel = Channel<Unit>(0)
And use channel.receive() for object.wait(), and channel.offer(Unit) for object.notify() (or send if you want to wait until the other coroutine receives).
For notifyAll, you can use BroadcastChannel instead.
You can of course easily encapsulate it:
inline class Waiter(private val channel: Channel<Unit> = Channel<Unit>(0)) {
suspend fun doWait() { channel.receive() }
fun doNotify() { channel.offer(Unit) }
}
It is possible to use the basic suspendCoroutine{..} function for that, e.g.
class SuspendWait() {
private lateinit var myCont: Continuation<Unit>
suspend fun sleepAndWait() = suspendCoroutine<Unit>{ cont ->
myCont = cont
}
fun resume() {
val cont = myCont
myCont = null
cont.resume(Unit)
}
}
It is clear, the code have issues, e.g. myCont field is not synchonized, it is expected that sleepAndWait is called before the resume and so on, hope the idea is clear now.
There is another solution with the Mutex class from the kotlinx.coroutines library.
class SuspendWait2 {
private val mutex = Mutex(locaked = true)
suspend fun sleepAndWait() = mutex.withLock{}
fun resume() {
mutex.unlock()
}
}
I suggest using a CompletableJob for that.
My use case:
suspend fun onLoad() {
var job1: CompletableJob? = Job()
var job2: CompletableJob? = Job()
lifecycleScope.launch {
someList.collect {
doSomething(it)
job1?.complete()
}
}
lifecycleScope.launch {
otherList.collect {
doSomethingElse(it)
job2?.complete()
}
}
joinAll(job1!!, job2!!) // suspends until both jobs are done
job1 = null
job2 = null
// Do something one time
}