Testing LiveData Transformations? - android

I've built a Splash Screen using Android Architecture Components and Reactive approach.
I return from Preferences LiveData object fun isFirstLaunchLD(): SharedPreferencesLiveData<Boolean>.
I have ViewModel that passes LiveData to the view and updates Preferences
val isFirstLaunch = Transformations.map(preferences.isFirstLaunchLD()) { isFirstLaunch ->
if (isFirstLaunch) {
preferences.isFirstLaunch = false
}
isFirstLaunch
}
In my Fragment, I observe LiveData from ViewModel
viewModel.isFirstLaunch.observe(this, Observer { isFirstLaunch ->
if (isFirstLaunch) {
animationView.playAnimation()
} else {
navigateNext()
}
})
I would like to test my ViewModel now to see if isFirstLaunch is updated properly. How can I test it? Have I separated all layers correctly? What kind of tests would you write on this sample code?

Have I separated all layers correctly?
The layers seem reasonably separated. The logic is in the ViewModel and you're not referring to storing Android Views/Fragments/Activities in the ViewModel.
What kind of tests would you write on this sample code?
When testing your ViewModel you can write instrumentation or pure unit tests on this code. For unit testing, you might need to figure out how to make a test double for preferences, so that you can focus on the isFirstLaunch/map behavior. An easy way to do that is passing a fake preference test double into the ViewModel.
How can I test it?
I wrote a little blurb on testing LiveData Transformations, read on!
Testing LiveData Transformations
Tl;DR You can test LiveData transformation, you just need to make sure the result LiveData of the Transformation is observed.
Fact 1: LiveData doesn't emit data if it's not observed. LiveData's "lifecycle awareness" is all about avoiding extra work. LiveData knows what lifecycle state it's observers (usually Activities/Fragments) are in. This allows LiveData to know if it's being observed by anything actually on-screen. If LiveData aren't observed or if their observers are off-screen, the observers are not triggered (an observer's onChanged method isn't called). This is useful because it keeps you from doing extra work "updating/displaying" an off-screen Fragment, for example.
Fact 2: LiveData generated by Transformations must be observed for the transformation to trigger. For Transformation to be triggered, the result LiveData (in this case, isFirstLaunch) must be observed. Again, without observation, the LiveData observers aren't triggered, and neither are the transformations.
When you're unit testing a ViewModel, you shouldn't have or need access to a Fragment/Activity. If you can't set up an observer the normal way, how do you unit test?
Fact 3: In your tests, you don't need a LifecycleOwner to observe LiveData, you can use observeForever You do not need a lifecycle observer to be able to test LiveData. This is confusing because generally outside of tests (ie in your production code), you'll use a LifecycleObserver like an Activity or Fragment.
In tests you can use the LiveData method observeForever() to observer without a lifecycle owner. This observer is "always" observing and doesn't have a concept of on/off screen since there's no LifecycleOwner. You must therefore manually remove the observer using removeObserver(observer).
Putting this all together, you can use observeForever to test your Transformations code:
class ViewModelTest {
// Executes each task synchronously using Architecture Components.
// For tests and required for LiveData to function deterministically!
#get:Rule
val rule = InstantTaskExecutorRule()
#Test
fun isFirstLaunchTest() {
// Create observer - no need for it to do anything!
val observer = Observer<Boolean> {}
try {
// Sets up the state you're testing for in the VM
// This affects the INPUT LiveData of the transformation
viewModel.someMethodThatAffectsFirstLaunchLiveData()
// Observe the OUTPUT LiveData forever
// Even though the observer itself doesn't do anything
// it ensures any map functions needed to calculate
// isFirstLaunch will be run.
viewModel.isFirstLaunch.observeForever(observer)
assertEquals(viewModel.isFirstLaunch.value, true)
} finally {
// Whatever happens, don't forget to remove the observer!
viewModel.isFirstLaunch.removeObserver(observer)
}
}
}
A few notes:
You need to use InstantTaskExecutorRule() to get your LiveData updates to execute synchronously. You'll need the androidx.arch.core:core-testing:<current-version> to use this rule.
While you'll often see observeForever in test code, it also sometimes makes its way into production code. Just keep in mind that when you're using observeForever in production code, you lose the benefits of lifecycle awareness. You must also make sure not to forget to remove the observer!
Finally, if you're writing a lot of these tests, the try, observe-catch-remove-code can get tedious. If you're using Kotlin, you can make an extension function that will simplify the code and avoid the possibility of forgetting to remove the observer. There are two options:
Option 1
/**
* Observes a [LiveData] until the `block` is done executing.
*/
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
val observer = Observer<T> { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
Which would make the test look like:
class ViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#Test
fun isFirstLaunchTest() {
viewModel.someMethodThatAffectsFirstLaunchLiveData()
// observeForTesting using the OUTPUT livedata
viewModel.isFirstLaunch.observeForTesting {
assertEquals(viewModel.isFirstLaunch.value, true)
}
}
}
Option 2
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this#getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
#Suppress("UNCHECKED_CAST")
return data as T
}
Which would make the test look like:
class ViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#Test
fun isFirstLaunchTest() {
viewModel.someMethodThatAffectsFirstLaunchLiveData()
// getOrAwaitValue using the OUTPUT livedata
assertEquals(viewModel.isFirstLaunch.getOrAwaitValue(), true)
}
}
These options were both taken from the reactive branch of Architecture Blueprints.

It depends on what your SharedPreferencesLiveData does.
If the SharedPreferencesLiveData contains Android specific classes, you won't be able to test this correctly because JUnit won't have access to the Android specific classes.
The other issue is that to be able to observe LiveData, you need some kind of Lifecycle owner. (The this in the original post code.)
In the Unit test, the 'this' can simply be replaced with something like the following:
private fun lifecycle(): Lifecycle {
val lifecycle = LifecycleRegistry(Mockito.mock(LifecycleOwner::class.java))
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
return lifecycle
}
And then used in the following way:
#RunWith(MockitoJUnitRunner::class)
class ViewModelTest {
#Rule
#JvmField
val liveDataImmediateRule = InstantTaskExecutorRule()
#Test
fun viewModelShouldLoadAttributeForConsent() {
var isLaunchedEvent: Boolean = False
// Pseudo code - Create ViewModel
viewModel.isFirstLaunch.observe(lifecycle(), Observer { isLaunchedEvent = it } )
assertEquals(true, isLaunchedEvent)
}
private fun lifecycle(): Lifecycle {
val lifecycle = LifecycleRegistry(Mockito.mock(LifecycleOwner::class.java))
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
return lifecycle
}
}
Note: You have to have the Rule present so that the LiveData executes instantly instead of whenever it wants to.

Related

ViewModel + Room test coverage in UnitTest

I have an unit test like this:
...
subj.mintToken(to, value, uri)
advanceUntilIdle()
...
val pendingTxFinalState = subj.uiState.value.pendingTx.count()
assertThat("Model should have a single pending tx, but has $pendingTxFinalState", pendingTxFinalState == 1)
...
The model field in ViewModel is populated by the request to cache in the init {} block. Each change in table would trigger this coroutine flow. This piece of unit test checks correctness of this functionality.
The current issue is this Flow in init {} block is triggered only on the test start when ViewModel instance is created. It does not respond on update in table.
It is important to note I don't use in test a room database neither test database, but FakeCacheRepository where behaviour of methods are emulated by flow with mocked data. However the behaviour of flow should be the same as there is still in change in underlying data.
val txPool = ConcurrentLinkedQueue<ITransaction>()
override fun createChainTx(tx: ITransaction): Flow<ITransaction> {
return flow {
txPool.add(tx)
emit(tx)
}
}
override fun getAllChainTransactions(): Flow<List<ITransaction>> {
return flow {
emit(txPool.toList())
}
}
Do you see the issue here or better way to test this?
My guess is you’re writing you’re own FakeCacheRepo and in the update function you are calling createChainTx. The value of the flow isn’t updating though because the create function doesn’t just update the value it creates a new flow instead of updating the old one. You can modify the set up to emit continuously in a loop (with some buffer delay) based on a variable. Then when you change the variable it will change what the current flow is emiting as expected.
The code example here is roughly doing that: https://developer.android.com/kotlin/flow#create
override fun createChainTx(): Flow<ITransaction> {
return flow {
while(true) {
val tx = getLatestTxValue() // Get the latest updated value from an outside source
txPool.add(tx)
emit(tx)
delay(refreshIntervalMs) // Suspends the coroutine for some time
}
}
}

Testing Kotlin flows by using toList() extension skips some emissions

I was testing some Kotlin Flows according to what the document mentioned in this part.
I created a FakeRepository and a FakeViewModel like this:
class FakeRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
val scores: Flow<Int> = flow
}
class FakeViewModel(private val repository: FakeRepository) : ViewModel() {
val score: StateFlow<Int> = repository.scores
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), 0)
fun initialize() {
viewModelScope.launch {
//delay(100)
repository.emit(1)
//delay(100)
repository.emit(2)
//delay(100)
repository.emit(3)
//delay(100)
}
}
}
I also created a test class to test this score flow like this:
#ExperimentalCoroutinesApi
class FakeViewModelTest {
/*
Replace real dispatchers with instances of TestDispatchers to ensure that all
code runs on the single test thread.
*/
#get:Rule
val mainDispatcherRule = MainDispatcherRule()
#Test
fun testFakeRepository() = runTest {
val fakeRepository = FakeRepository()
val viewModel = FakeViewModel(fakeRepository)
val items = mutableListOf<Int>()
val job = launch(StandardTestDispatcher()) {
viewModel.score.toList(items)
}
viewModel.initialize()
advanceUntilIdle()
MatcherAssert.assertThat(items.size, Matchers.equalTo(4))
job.cancel()
}
}
When I run the test, surprisingly, it fails and says that the list size is 1.
But after uncommenting the delays, the test will be passed.
Why the test behave like this? As the document mentioned here, TestDispatcher will skip delays.
Did I misunderstand something? How can I fix it?
As the StateFlow documentation says:
Updates to the value are always conflated. So a slow collector skips fast updates, but always collects the most recently emitted value.
A StateFlow has no memory whatsoever of previous values. In your current code, the collector and the initialize function's coroutine are racing each other. There's no guarantee that the emitter will wait for an item to be collected before emitting the next.
The possible reason the delay calls make it work is that even with the TestDispatcher, it might be suspending (yielding the thread), which would give the other coroutine a chance to collect each item. Not sure, because I don't know the details of what these TestDispatchers do under the hood.
If you want to guarantee values aren't dropped, you need to use a SharedFlow with BufferOverflow.SUSPEND. Or you could give it a replay of Int.MAX_VALUE, but this could be unsustainable depending on your use case.

Is livedata builder ok for one-shot operations?

For example, let's say that we have a product catalog view with an option to add product to a cart.
Each time when user clicks add to cart, a viewModel method addToCart is called, that could look like this:
//inside viewModel
fun addToCart(item:Item): LiveData<Result> = liveData {
val result = repository.addToCart(item) // loadUser is a suspend function.
emit(result)
}
//inside view
addButton.onClickListener = {
viewModel.addToCart(selectedItem).observe (viewLifecycleOwner, Observer () {
result -> //show result
}
}
What happens after adding for example, 5 items -> will there be 5 livedata objects in memory observed by the view?
If yes, when will they be cleanup? And if yes, should we avoid livedata builder for one-shot operations that can be called multiple times?
Your implementation seems wrong! You are constantly returning a new LiveData object for every addToCard function call. About your first question, it's a Yes.
If you want to do it correctly via liveData.
// In ViewModel
private val _result = MutableLiveData<Result>()
val result: LiveData<Result>
get() = _result;
fun addToCart(item: Item) {
viewModelScope.launch {
// Call suspend functions
result.value = ...
}
}
// Activity/Fragment
viewModel.result.observe(lifecycleOwner) { result ->
// Process the result
...
}
viewModel.addToCart(selectedItem)
All you have to do is call it from activity & process the result. You can also use StateFlow for this purpose. It also has an extension asLiveData which converts Flow -> LiveData as well.
According to LiveData implementation of:
public void observe(#NonNull LifecycleOwner owner, #NonNull Observer<? super T> observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
// ignore
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer"
+ " with different lifecycles");
}
if (existing != null) {
return;
}
owner.getLifecycle().addObserver(wrapper);
}
a new Observer (wrapper) is added every time you observe a LiveData. Looking at this I would be carefull creating new Observers from a view (click) event. At the moment I can not tell if a Garbage Collector can free this resources.
As #kaustubhpatange mentioned, you should have one LiveData with a state/value that can be changed by the viewModel, with every new result. That LiveData can be observed (once) in your Activity or Fragment onCreate() function:
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.result.observe(lifecycleOwner) { result ->
// handle the result
}
}
Using MutableLiveData in your ViewModel, you can mostly create LiveData only once, and populate it later with values from click events, responses etc.
TL;DR
If your operation is One-Shot use Coroutine and LiveData.
If your operation serving with Streams you can use Flow.
For one-shot operations, your approach it's OK.
I think with liveData builder there is no any Memory leak.
If you use for example private backing property for LiveData and observe an public LiveData it might occurs different behavior like get latest value before assign new value to that.

Android architecture components and coroutines

There is a lot of information out there on architecture components, kotlin and coroutines but nowhere I can find an example using all those things together.
I'm struggling on how to use android's architecture components as described here together with coroutines. I have an idea but feel uncertain if it's the correct way of implementating this architectural style.
I'm trying to use the view model + repository pattern together with retro fit and coroutines.
I have the following repository:
class FooRepostiroy(private val fooHttpService: FooHttpService) {
suspend fun someMethod() : SomeResult {
val response = fooHttpService.someRemotCall() // which is also a suspending method using retrofit-2
// process response, store it using room and return SomeResult data object
Then I use the FooRepository from my ViewModel but because someMethod is a suspending method I need to wrap it in a coroutine scope:
class FooViewModel(private val fooRepositoru : FooRepository) : ViewModel() {
private var someMethodJob : Job? = null
val result : MutableLiveData<SomeResult> = MutableLiveData()
fun someMethod() {
someMethodJob = viewModelScope.launch {
result.value = fooRepositoru.someMethod()
}
}
override fun onCleared() {
super.onCleared()
someMethodJob?.cancel()
}
Then in the fragment or activity I can observe the view model result
fooViewModel.result.observe(viewLifecycleOwner, Observer {
Starting from my repository layer and below everything can be a suspending function. Then from the view model I can call any suspending function but never have a publicly exposed suspending function in my view model.
Is this the correct or proper way to incorporate coroutines with the view model architecture ?
Is this the correct or proper way to incorporate coroutines with the view model architecture?
Yes!
Every instance of ViewModel has its own ViewModelScope.
The purpose of ViewModelScope is to run the jobs during the life cycle of that ViewModel and take care of automatic cancelation of running coroutine jobs in case the parent Activity/Fragment of ViewModel is destroyed.
Any running jobs under ViewModelScope will be canceled when the ViewModel will be destroyed.
Read more here
private var someMethodJob : Job? = null
val result : MutableLiveData<SomeResult> = MutableLiveData()
fun someMethod() {
someMethodJob = viewModelScope.launch {
result.value = fooRepositoru.someMethod()
}
}
You can ditch all of that and just say
val result: LiveData<SomeResult> = liveData {
emit(fooRepository.someMethod())
}
And then observe result.

How can i restart livedata builder function?

I'm trying to use livedata builder functions. Indeed, it's so easy in use, but actually I can't understand how I can restart my coroutine. Below my part of code:
val topStoriesResult : LiveData<UIState<TopStoryWrapper>> = liveData(Dispatchers.IO) {
topStoriesRepository.getTopStoriesSetWrapper().apply {
emit(UIState.Loading)
onFailure { emit(UIState.NoData) }
onSuccess { emit(UIState.HasData(it)) }
}
}
liveData builder can't be restarted, docs say:
The liveData building block serves as a structured concurrency primitive between coroutines and LiveData. The code block starts executing when LiveData becomes active and is automatically canceled after a configurable timeout when the LiveData becomes inactive. If it is canceled before completion, it is restarted if the LiveData becomes active again. If it completed successfully in a previous run, it doesn't restart. Note that it is restarted only if canceled automatically. If the block is canceled for any other reason (e.g. throwing a CancelationException), it is not restarted.
To make code run a couple of times I can suggest to create a function and call it when you need, e.g. on button click:
class MainViewModel : ViewModel() {
val topStoriesResult: LiveData<UIState<TopStoryWrapper>> = MutableLiveData<UIState<TopStoryWrapper>>()
fun loadTopStories() = viewModelScope.launch(Dispatchers.IO) { // start a coroutine
topStoriesRepository.getTopStoriesSetWrapper().apply {
val mutableLiveData = loginResponse as MutableLiveData
// post value to LiveData
mutableLiveData.postValue(UIState.Loading)
onFailure { mutableLiveData.postValue(UIState.NoData) }
onSuccess { mutableLiveData.postValue(UIState.HasData(it)) }
}
}
}
To use viewModelScope in MainViewModel class add dependency to build.gradle file:
final LIFECYCLE_VERSION = "2.2.0-rc03" // add most recent version
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION"

Categories

Resources