How to disable Spring Animation for Espresso Tests? - android

I'm using the Android spring animation in my project (see here). However, these animations are getting in the way of my espresso tests.
I already tried to disable these animations using the developer options in the phone, but they seem to not be affected by these settings.
Is there any way how I can disable them just for tests?

After struggling with a flaky test due to SpringAnimations I came up with three solutions:
Solution 1: Add a function that wraps creating your SpringAnimations
This is the most invasive in terms of changing existing code, but least complex method to follow:
You can check if animations are disabled at runtime:
fun animationsDisabled() =
Settings.Global.getFloat(
contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f,
) == 0.0f
Then selectively return a dummy animation that immediately finishes while also setting the value to it's final state:
fun <K : View?> createAnimation(
target: K,
property: FloatPropertyCompat<K>,
finalValue: Float
) = if (animationsDisabled() == false) {
SpringAnimation(target, property, finalValue).apply {
spring.dampingRatio = dampingRatio
spring.stiffness = stiffness
}
} else {
property.setValue(target, finalValue)
SpringAnimation(FloatValueHolder(0f)).apply{
spring = SpringForce(100f)
spring.dampingRatio = dampingRatio
spring.stiffness = stiffness
addUpdateListener { _, _, _ -> skipToEnd() }
}
}
}
Solution 2: Create an IdlingResource that tells Espresso if a DynamicAnimation is running
SpringAnimation and FlingAnimation both extend from DynamicAnimation, the class which is ignoring the system Animation Scale and causing issues here.
This solution isn't the prettiest as it uses reflection, but the implementation details it relies on haven't changed since DynamicAnimation was introduced.
Based on DataBindingIdlingResource:
import android.view.View
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.test.espresso.IdlingResource
import androidx.test.ext.junit.rules.ActivityScenarioRule
import java.util.UUID
// An espresso idling resource implementation that reports idle status for all DynamicAnimation instances
class DynamicAnimationIdlingResource(private val activityScenarioRule: ActivityScenarioRule<*>) :
IdlingResource {
// list of registered callbacks
private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()
// give it a unique id to workaround an espresso bug where you cannot register/unregister
// an idling resource w/ the same name.
private val id = UUID.randomUUID().toString()
// holds whether isIdle is called and the result was false. We track this to avoid calling
// onTransitionToIdle callbacks if Espresso never thought we were idle in the first place.
private var wasNotIdle = false
override fun getName() = "DynamicAnimation $id"
override fun isIdleNow(): Boolean {
val idle = !getDynamicAnimations().any { it.isRunning }
#Suppress("LiftReturnOrAssignment")
if (idle) {
if (wasNotIdle) {
// notify observers to avoid espresso race detector
idlingCallbacks.forEach { it.onTransitionToIdle() }
}
wasNotIdle = false
} else {
wasNotIdle = true
activityScenarioRule.scenario.onActivity {
it.findViewById<View>(android.R.id.content)
.postDelayed({ isIdleNow }, 16)
}
}
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
idlingCallbacks.add(callback)
}
/**
* Find all binding classes in all currently available fragments.
*/
private fun getDynamicAnimations(): List<DynamicAnimation<*>> {
val dynamicAnimations = mutableListOf<DynamicAnimation<*>>()
val animationHandlerClass = Class
.forName("androidx.dynamicanimation.animation.AnimationHandler")
val animationHandler =
animationHandlerClass
.getDeclaredMethod("getInstance")
.invoke(null)
val animationCallbacksField =
animationHandlerClass
.getDeclaredField("mAnimationCallbacks").apply {
isAccessible = true
}
val animationCallbacks =
animationCallbacksField.get(animationHandler) as ArrayList<*>
animationCallbacks.forEach {
if (it is DynamicAnimation<*>) {
dynamicAnimations.add(it)
}
}
return dynamicAnimations
}
}
For convenience a matching test rule:
/**
* A JUnit rule that registers an idling resource for all animations that use DynamicAnimations.
*/
class DynamicAnimationIdlingResourceRule(activityScenarioRule: ActivityScenarioRule<*>) : TestWatcher() {
private val idlingResource = DynamicAnimationIdlingResource(activityScenarioRule)
override fun finished(description: Description?) {
IdlingRegistry.getInstance().unregister(idlingResource)
super.finished(description)
}
override fun starting(description: Description?) {
IdlingRegistry.getInstance().register(idlingResource)
super.starting(description)
}
}
This isn't the perfect solution since it will still cause your tests to wait for animations despite changing the animation scale globally
If you have infinite animations based on SpringAnimations (by setting Damping to zero), this won't work as it'll always report to Espresso that an animation is running. You could work around that by casting the DynamicAnimation to a SpringAnimation and checking if Damping was set, but I felt like that's a rare enough case to not complicate things.
Solution 3: Force all SpringAnimations to skip to their last frame
Another reflection based solution, but this one completely disables the SpringAnimations. The trade-off is that theoretically Espresso can still try to interact in the 1 frame window between a SpringAnimation being asked to end, and it actually ending.
In practice I had to rerun the test hundreds of times in a row to get this to happen, at which point the animation may not even be the source of flakiness. So the trade-off is probably worth it if the animations are dragging down how long your tests take to complete:
private fun disableSpringAnimations() {
val animationHandlerClass = Class
.forName("androidx.dynamicanimation.animation.AnimationHandler")
val animationHandler =
animationHandlerClass
.getDeclaredMethod("getInstance")
.invoke(null)
val animationCallbacksField =
animationHandlerClass
.getDeclaredField("mAnimationCallbacks").apply {
isAccessible = true
}
CoroutineScope(Dispatchers.IO).launch {
while (true) {
withContext(Dispatchers.Main) {
val animationCallbacks =
animationCallbacksField.get(animationHandler) as ArrayList<*>
animationCallbacks.forEach {
val animation = it as? SpringAnimation
if (animation?.isRunning == true && animation.canSkipToEnd()) {
animation.skipToEnd()
animation.doAnimationFrame(100000L)
}
}
}
delay(16L)
}
}
}
Call this method in your #Before annotated function to have it run before each test.
In the SpringAnimation implementation, skipToEnd sets a flag that is not checked until the next call to doAnimationFrame, hence the animation.doAnimationFrame(100000L) call.

Related

Updating MutableStateFlow without emitting to collectors

In an Android project, we are currently trying to switch from LiveData to StateFlow in our viewmodels. But for some rare cases, we need to update our state without notifying the collectors about the change. It might sound weird when we think of the working mechanism of flows, but I want to learn if it's a doable thing or not. Any real solution or workaround would be appreciated.
If you don't need to react to the true state anywhere, but only the publicly emitted state, I would store the true state in a property directly instead of a MutableStateFlow.
private var trueState: MyState = MyState(someDefault)
private val _publicState = MutableStateFlow<MyState>()
val publicstate = _publicState.asStateFlow()
fun updateState(newState: MyState, shouldEmitPublicly: Boolean) {
trueState = newState
if (shouldEmitPublicly) {
_publicState.value = newState
}
}
If you do need to react to it, one alternative to a wrapper class and filtering (#broot's solution) would be to simply keep two separate StateFlows.
Instead of exposing the state flow directly, we can expose another flow that filters the items according to our needs.
For example, we can keep the shouldEmit flag inside emitted items. Or use any other filtering logic:
suspend fun main(): Unit = coroutineScope {
launch {
stateFlow.collect {
println("Collected: $it")
}
}
delay(100)
setState(1)
delay(100)
setState(2)
delay(100)
setState(3, shouldEmit = false)
delay(100)
setState(4)
delay(100)
setState(5)
delay(100)
}
private val _stateFlow = MutableStateFlow(EmittableValue(0))
val stateFlow = _stateFlow.filter { it.shouldEmit }
.map { it.value }
fun setState(value: Int, shouldEmit: Boolean = true) {
_stateFlow.value = EmittableValue(value, shouldEmit)
}
private data class EmittableValue<T>(
val value: T,
val shouldEmit: Boolean = true
)
We can also keep the shouldEmit flag in the object and switch it on/off to temporarily disable emissions.
If you need to expose StateFlow and not just Flow, this should also be possible, but you need to decide if ignored emissions should affect its value or not.

Android Espresso - How to check if a view is ready to be clicked

I have an app with a splashscreen, which stays for about 2 seconds.
After that, it switches to another activity A.
In A, I set a value in a SeekBar and after that, click a Button to confirm.
When I simply start a recorded Espresso test doing this, it tries to play while on the splashscreen. So when it tried to set the SeekBar value or click the Button, I get a NoMatchingViewException. So my first attempt at fixing this was to simply add a sleep(5000). This worked.
However, I don't want to put a manual sleep in after every Activity switch.
Because it seems like unnecessary code
Because it would mean unnecessary waiting time for running the test
The timing might be arbitrary and could be different for different devices
So I tried to check whether or not I'm in the right Activity/can see the right views. I did this using some SO links: Wait for Activity and Wait for View.
However, even that does not work 100%.
I have these two functions:
fun <T: AppCompatActivity> waitForActivity(activity: Class<T>, timeout: Int = 5000, waitTime: Int = 100) {
val maxTries = timeout / waitTime
var tries = 0
for(i in 0..maxTries) {
var currentActivity: Activity? = null
getInstrumentation().runOnMainSync { run { currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(
Stage.RESUMED).elementAtOrNull(0) } }
if(activity.isInstance(currentActivity)) {
break
} else {
tries++
sleep(waitTime.toLong())
}
}
}
fun waitForView(
#IntegerRes id: Int,
waitMillis: Int = 5000,
waitMillisPerTry: Long = 100
): ViewInteraction {
// Derive the max tries
val viewMatcher = allOf(
withId(id),
isDisplayed()
)
val maxTries = waitMillis / waitMillisPerTry.toInt()
var tries = 0
for (i in 0..maxTries)
try {
tries++
val element = onView(viewMatcher)
element.check { view, noViewFoundException ->
if(view == null) {
throw noViewFoundException ?: Exception("TEST")
}
if(view.hasWindowFocus()) {
throw noViewFoundException ?: Exception("TEST2")
}
}
return element
} catch (e: Exception) {
if (tries == maxTries) {
throw e
}
sleep(waitMillisPerTry)
}
throw Exception("Error finding a view matching $viewMatcher")
}
Neither of those work 100%. Both of them seem to return within the timeout restrictions, and have "found" the activity/view. However, the expected view, e.g. a Button is not yet ready to perform, for example, element.perform(click()). It does not lead to a NoMatchingViewException, but it does not perform the click I did either. For the SeekBar, I use this:
private fun setSeekValue(seekBar: ViewInteraction, age: Int) {
val fullPercentage = .9f
val step = 1/99f
seekBar.perform(
GeneralClickAction(
Tap.SINGLE,
CoordinatesProvider { view ->
val pos = IntArray(2)
view?.getLocationOnScreen(pos)
FloatArray(2).apply {
this[0] = pos[0] + view!!.width * (.05f + fullPercentage*step*age)
this[1] = pos[1] + view.height * .5f
}
},
PrecisionDescriber {
FloatArray(2).apply {
this[0] = .1f
this[1] = 1f
}
},
InputDevice.SOURCE_MOUSE,
MotionEvent.ACTION_BUTTON_PRESS
)
)
}
However, when I use these functions and just put a very short sleep, e.g. sleep(100) after it, it works. This again however, would go against the three reasons listed above, which I'm trying to avoid here.
As you can see in the function waitForView, I tried to check if the View is "usable", using hasWindowFocus(). But this still does not perform the click, except for when I again put a sleep(80) or something after it. So it waits for the splashscreen to switch to A, finds the view it's looking for and then can't perform the click, except for when I wait a little bit.
I have also tried these functions of View:
isEnabled
isShown
visibility
getDrawingRect
isFocusable
isFocused
isLayoutDirectionResolved
Neither of them worked as I expected. With all of them, after the needed value was returned on the element.check part of waitForView, they would still not be accessible without putting a short sleep after.
Is there a way to reliably check if I can perform a click on a view/safely can perform ViewInteraction.perform()
Either by checking, if an activity is fully loaded to a point where its views are usable. Or by directly checking if a view is usable.

How can I debounce a setOnClickListener for 1 second using Kotlin Coroutines?

When user taps fast on the button the showDialog() method displays multiple times on top of each other, so when you dismiss it there is another one behind it. I am looking for a way to ignore the second tap for 1 second without using a handler or check the previous tap's time.
//Button that opens a dialog
button.setOnClickListener {
showDialog()
}
I am looking for a solution using Kotlin coroutines or Kotlin flows for future implementations.
It's better to use a simple Flag for that instead of delay as it's not a good user experience.
But if you want to use Coroutines, You can simply use Kotlin Coroutine's Flow to apply this:
First I created an Extension Function for the click event that returns a Coroutine's Flow. like this:
fun View.clicks(): Flow<Unit> = callbackFlow {
setOnClickListener {
offer(Unit)
}
awaitClose { setOnClickListener(null) }
}
Now, All you need is Calling your Function in onCreate like this:
button.clicks().debounce(1000).onEach { println("clicked") }.launchIn(GlobalScope)
Don't forget to add these lines in build.gradle file:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
Edit:
The Flow analogue of throttleFirst operator is not implemented yet in kotlin coroutines. however, can be implemented with the help of Extension Functions:
#FlowPreview
#ExperimentalCoroutinesApi
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
var lastEmissionTime = 0L
collect { upstream ->
val currentTime = System.currentTimeMillis()
val mayEmit = currentTime - lastEmissionTime > windowDuration
if (mayEmit)
{
lastEmissionTime = currentTime
emit(upstream)
}
}
}
The changes are as follows:
binding.button.clicks().throttleFirst(1250)
.onEach {
//delay(100)
showDialog()
}.launchIn(GlobalScope)
Also, you can use a delay() to handle this. Take it easy to change value of these parameters according to your needs.
Couroutines are overkill for something as trivial as debounce:
class DebounceOnClickListener(
private val interval: Long,
private val listenerBlock: (View) -> Unit
): View.OnClickListener {
private var lastClickTime = 0L
override fun onClick(v: View) {
val time = System.currentTimeMillis()
if (time - lastClickTime >= interval) {
lastClickTime = time
listenerBlock(v)
}
}
}
fun View.setOnClickListener(debounceInterval: Long, listenerBlock: (View) -> Unit) =
setOnClickListener(DebounceOnClickListener(debounceInterval, listenerBlock))
Usage:
myButton.setOnClickListener(1000L) { doSomething() }
I honestly recommend corbind
With this great library you can forget about setOnClickListener and just handle flows like
binding.myButton.clicks()
.debounce(500)
.onEach { doSomethingImportant() }
.launchIn(viewLifecycleOwner.lifecycleScope)
It's really easy to use, and with view binding making an app becomes super simple. I hope it helps, happy coding!
To improve Morteza Nedaei's solution:
If you try that code within a timeframe of 2000ms and a window duration of 500ms, it will be triggered only 4 times; while it should be 5 times.
Morteza's:
237: #1-(0-500)
839: #2-(501-1000)
1440: #3-(1001-1500)
????: #?
2041: #5-(2001-2500)
Improved one:
236: #1-(0-500)
637: #2-(501-1000)
1038: #3-(1001-1500)
1639: #4-(1501-2000)
2039: #5-(2001-2500)
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
var windowStartTime = System.currentTimeMillis()
var isEmitted = false
collect { value ->
val currentTime = System.currentTimeMillis()
val delta = currentTime - windowStartTime
if (delta >= windowDuration) {
windowStartTime += delta / windowDuration * windowDuration
isEmitted = false
}
if (isEmitted.not()) {
emit(value)
isEmitted = true
}
}
}

Communication between view and ViewModel in MVVM with LiveData

What is a proper way to communicate between the ViewModel and the View, Google architecture components give use LiveData in which the view subscribes to the changes and update itself accordingly, but this communication not suitable for single events, for example show message, show progress, hide progress etc.
There are some hacks like SingleLiveEvent in Googles example but it work only for 1 observer.
Some developers using EventBus but i think it can quickly get out of control when the project grows.
Is there a convenience and correct way to implement it, how do you implement it?
(Java examples welcome too)
Yeah I agree, SingleLiveEvent is a hacky solution and EventBus (in my experience) always lead to trouble.
I found a class called ConsumableValue a while back when reading the Google CodeLabs for Kotlin Coroutines, and I found it to be a good, clean solution that has served me well (ConsumableValue.kt):
class ConsumableValue<T>(private val data: T) {
private var consumed = false
/**
* Process this event, will only be called once
*/
#UiThread
fun handle(block: ConsumableValue<T>.(T) -> Unit) {
val wasConsumed = consumed
consumed = true
if (!wasConsumed) {
this.block(data)
}
}
/**
* Inside a handle lambda, you may call this if you discover that you cannot handle
* the event right now. It will mark the event as available to be handled by another handler.
*/
#UiThread
fun ConsumableValue<T>.markUnhandled() {
consumed = false
}
}
class MyViewModel : ViewModel {
private val _oneShotEvent = MutableLiveData<ConsumableValue<String>>()
val oneShotEvent: LiveData<ConsumableValue<String>>() = _oneShotData
fun fireEvent(msg: String) {
_oneShotEvent.value = ConsumableValue(msg)
}
}
// In Fragment or Activity
viewModel.oneShotEvent.observe(this, Observer { value ->
value?.handle { Log("TAG", "Message:$it")}
})
In short, the handle {...} block will only be called once, so there's no need for clearing the value if you return to a screen.
What about using Kotlin Flow?
I do not believe they have the same behavior that LiveData has where it would alway give you the latest value. Its just a subscription similar to the workaround SingleLiveEvent for LiveData.
Here is a video explaining the difference that I think you will find interesting and answer your questions
https://youtu.be/B8ppnjGPAGE?t=535
try this:
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
And wrapper it into LiveData
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
And observe
myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
link reference: Use an Event wrapper
For showing/hiding progress dialogs and showing error messages from a failed network call on loading of the screen, you can use a wrapper that encapsulates the LiveData that the View is observing.
Details about this method are in the addendum to app architecture:
https://developer.android.com/jetpack/docs/guide#addendum
Define a Resource:
data class Resource<out T> constructor(
val state: ResourceState,
val data: T? = null,
val message: String? = null
)
And a ResourceState:
sealed class ResourceState {
object LOADING : ResourceState()
object SUCCESS : ResourceState()
object ERROR : ResourceState()
}
In the ViewModel, define your LiveData with the model wrapped in a Resource:
val exampleLiveData = MutableLiveData<Resource<ExampleModel>>()
Also in the ViewModel, define the method that makes the API call to load the data for the current screen:
fun loadDataForView() = compositeDisposable.add(
exampleUseCase.exampleApiCall()
.doOnSubscribe {
exampleLiveData.setLoading()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
exampleLiveData.setSuccess(it)
},
{
exampleLiveData.setError(it.message)
}
)
)
In the View, set up the Observer on creation:
viewModel.exampleLiveData.observe(this, Observer {
updateResponse(it)
})
Here is the example updateResponse() method, showing/hiding progress, and showing an error if appropriate:
private fun updateResponse(resource: Resource<ExampleModel>?) {
resource?.let {
when (it.state) {
ResourceState.LOADING -> {
showProgress()
}
ResourceState.SUCCESS -> {
hideProgress()
// Use data to populate data on screen
// it.data will have the data of type ExampleModel
}
ResourceState.ERROR -> {
hideProgress()
// Show error message
// it.message will have the error message
}
}
}
}
You can easily achieve this by not using LiveData, and instead using Event-Emitter library that I wrote specifically to solve this problem without relying on LiveData (which is an anti-pattern outlined by Google, and I am not aware of any other relevant alternatives).
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
implementation 'com.github.Zhuinden:event-emitter:1.0.0'
If you also copy the LiveEvent class , then now you can do
private val emitter: EventEmitter<String> = EventEmitter()
val events: EventSource<String> get() = emitter
fun doSomething() {
emitter.emit("hello")
}
And
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = getViewModel<MyViewModel>()
viewModel.events.observe(viewLifecycleOwner) { event ->
// ...
}
}
// inline fun <reified T: ViewModel> Fragment.getViewModel(): T = ViewModelProviders.of(this).get(T::class.java)
For rationale, you can check out my article I wrote to explain why the alternatives aren't as valid approaches.
You can however nowadays also use a Channel(UNLIMITED) and expose it as a flow using asFlow(). That wasn't really applicable back in 2019.

Flakiness in tests on Android using LiveData, RxJava/RxKotlin and Spek

Setup:
In our project (at work - I cannot post real code), we have implemented clean MVVM. Views communicate with ViewModels via LiveData. ViewModel hosts two kinds of use cases: 'action use cases' to do something, and 'state updater use cases'. Backward communication is asynchronous (in terms of action reaction). It's not like an API call where you get the result from the call. It's BLE, so after writing the characteristic there will be a notification characteristic we listen to. So we use a lot of Rx to update the state. It's in Kotlin.
ViewModel:
#PerFragment
class SomeViewModel #Inject constructor(private val someActionUseCase: SomeActionUseCase,
someUpdateStateUseCase: SomeUpdateStateUseCase) : ViewModel() {
private val someState = MutableLiveData<SomeState>()
private val stateSubscription: Disposable
// region Lifecycle
init {
stateSubscription = someUpdateStateUseCase.state()
.subscribeIoObserveMain() // extension function
.subscribe { newState ->
someState.value = newState
})
}
override fun onCleared() {
stateSubscription.dispose()
super.onCleared()
}
// endregion
// region Public Functions
fun someState() = someState
fun someAction(someValue: Boolean) {
val someNewValue = if (someValue) "This" else "That"
someActionUseCase.someAction(someNewValue)
}
// endregion
}
Update state use case:
#Singleton
class UpdateSomeStateUseCase #Inject constructor(
private var state: SomeState = initialState) {
private val statePublisher: PublishProcessor<SomeState> =
PublishProcessor.create()
fun update(state: SomeState) {
this.state = state
statePublisher.onNext(state)
}
fun state(): Observable<SomeState> = statePublisher.toObservable()
.startWith(state)
}
We are using Spek for unit tests.
#RunWith(JUnitPlatform::class)
class SomeViewModelTest : SubjectSpek<SomeViewModel>({
setRxSchedulersTrampolineOnMain()
var mockSomeActionUseCase = mock<SomeActionUseCase>()
var mockSomeUpdateStateUseCase = mock<SomeUpdateStateUseCase>()
var liveState = MutableLiveData<SomeState>()
val initialState = SomeState(initialValue)
val newState = SomeState(newValue)
val behaviorSubject = BehaviorSubject.createDefault(initialState)
subject {
mockSomeActionUseCase = mock()
mockSomeUpdateStateUseCase = mock()
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(behaviorSubject)
SomeViewModel(mockSomeActionUseCase, mockSomeUpdateStateUseCase).apply {
liveState = state() as MutableLiveData<SomeState>
}
}
beforeGroup { setTestRxAndLiveData() }
afterGroup { resetTestRxAndLiveData() }
context("some screen") {
given("the action to open the screen") {
on("screen opened") {
subject
behaviorSubject.startWith(initialState)
it("displays the initial state") {
assertEquals(liveState.value, initialState)
}
}
}
given("some setup") {
on("some action") {
it("does something") {
subject.doSomething(someValue)
verify(mockSomeUpdateStateUseCase).someAction(someOtherValue)
}
}
on("action updating the state") {
it("displays new state") {
behaviorSubject.onNext(newState)
assertEquals(liveState.value, newState)
}
}
}
}
}
At first we were using an Observable instead of the BehaviorSubject:
var observable = Observable.just(initialState)
...
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(observable)
...
observable = Observable.just(newState)
assertEquals(liveState.value, newState)
instead of the:
val behaviorSubject = BehaviorSubject.createDefault(initialState)
...
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(behaviorSubject)
...
behaviorSubject.onNext(newState)
assertEquals(liveState.value, newState)
but the unit test were being flaky. Mostly they would pass (always when ran in isolation), but sometime they would fail when running the whole suit. Thinking it is to do with asynchronous nature of the Rx we moved to BehaviourSubject to be able to control when the onNext() happens. Test are now passing when we run them from AndroidStudio on the local machine, but they are still flaky on the build machine. Restarting the build often makes them pass.
The tests which fail are always the ones where we assert the value of LiveData. So the suspects are LiveData, Rx, Spek or their combination.
Question: Did anyone have similar experiences writing unit tests with LiveData, using Spek or maybe Rx, and did you find ways to write them which solve these flakiness issues?
....................
Helper and extension functions used:
fun instantTaskExecutorRuleStart() =
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) {
runnable.run()
}
override fun isMainThread(): Boolean {
return true
}
override fun postToMainThread(runnable: Runnable) {
runnable.run()
}
})
fun instantTaskExecutorRuleFinish() = ArchTaskExecutor.getInstance().setDelegate(null)
fun setRxSchedulersTrampolineOnMain() = RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
fun setTestRxAndLiveData() {
setRxSchedulersTrampolineOnMain()
instantTaskExecutorRuleStart()
}
fun resetTestRxAndLiveData() {
RxAndroidPlugins.reset()
instantTaskExecutorRuleFinish()
}
fun <T> Observable<T>.subscribeIoObserveMain(): Observable<T> =
subscribeOnIoThread().observeOnMainThread()
fun <T> Observable<T>.subscribeOnIoThread(): Observable<T> = subscribeOn(Schedulers.io())
fun <T> Observable<T>.observeOnMainThread(): Observable<T> =
observeOn(AndroidSchedulers.mainThread())
I didn't used Speck for unit-testing. I've used java unit-test platform and it works perfect with Rx & LiveData, but you have to keep in mind one thing. Rx & LiveData are async and you can't do something like someObserver.subscribe{}, someObserver.doSmth{}, assert{} this will work sometimes but it's not the correct way to do it.
For Rx there's TestObservers for observing Rx events. Something like:
#Test
public void testMethod() {
TestObserver<SomeObject> observer = new TestObserver()
someClass.doSomethingThatReturnsObserver().subscribe(observer)
observer.assertError(...)
// or
observer.awaitTerminalEvent(1, TimeUnit.SECONDS)
observer.assertValue(somethingReturnedForOnNext)
}
For LiveData also, you'll have to use CountDownLatch to wait for LiveData execution. Something like this:
#Test
public void someLiveDataTest() {
CountDownLatch latch = new CountDownLatch(1); // if you want to check one time exec
somethingTahtReturnsLiveData.observeForever(params -> {
/// you can take the params value here
latch.countDown();
}
//trigger live data here
....
latch.await(1, TimeUnit.SECONDS)
assert(...)
}
Using this approach your test should run ok in any order on any machine. Also the wait time for latch & terminal event should be as low as possible, the tests should run fast.
Note1: The code is in JAVA but you can change it easily in kotlin.
Note2: Singleton are the biggest enemy of unit-testing ;). (With static methods by their side).
The issue is not with LiveData; it is the more common problem - singletons. Here the Update...StateUseCases had to be singletons; otherwise if observers got a different instance they would have a different PublishProcessor and would not get what was published.
There is a test for each Update...StateUseCases and there is a test for each ViewModel into which Update...StateUseCases is injected (well indirectly via the ...StateObserver).
The state exists within the Update...StateUseCases, and since it is a singleton, it gets changed in both tests and they use the same instance becoming dependent on each other.
Firstly try to avoid using singletons if possible.
If not, reset the state after each test group.

Categories

Resources