Skip to content

Instantly share code, notes, and snippets.

@ConorGarry
Created November 8, 2021 16:28
Show Gist options
  • Save ConorGarry/26d4b62e98d17ccb3ff36c1722d3cbca to your computer and use it in GitHub Desktop.
Save ConorGarry/26d4b62e98d17ccb3ff36c1722d3cbca to your computer and use it in GitHub Desktop.
LiveData test extensions.
package ie.conorgarry.patreontemplate
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
// kotlin-coroutines-end/app/src/test/java/com/example/android/kotlincoroutines/LiveDataTestUtil.kt
/**
* Reminder that this is needed to run LiveData unit tests.
* ```
* @get:Rule // Required for LiveData unit test.
* public var instantExecutorRule = InstantTaskExecutorRule()
* ```
*/
object LiveDataTestUtil {
/**
* Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds.
* Once we got a notification via onChanged, we stop observing.
*/
fun <T> getValue(liveData: LiveData<T>): T {
val data = arrayOfNulls<Any>(1)
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data[0] = o
latch.countDown()
liveData.removeObserver(this)
}
}
liveData.observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
@Suppress("UNCHECKED_CAST")
return data[0] as T
}
}
@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()
[email protected](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
}
class LiveDataValueCapture<T> {
private val lock = Any()
private val _values = mutableListOf<T?>()
val values: List<T?>
get() = synchronized(lock) {
_values.toList() // copy to avoid returning reference to mutable list
}
fun addValue(value: T?) = synchronized(lock) {
_values += value
}
}
/**
* Slightly modified version of [this][https://stackoverflow.com/a/60951065/3429021]
*
* @param expected Wait for the expected amount of states to happen.
* Defaults to 2 as it will most likely either be
* a loading state followed by either a success or error.
*/
inline fun <T> LiveData<T>.captureValues(
expected: Int = 2,
block: LiveDataValueCapture<T>.() -> Unit
) {
val capture = LiveDataValueCapture<T>()
val observer = Observer<T> {
capture.addValue(it)
}
observeForever(observer)
try {
// Wait for all states to happen. Without this, tests using OkHttp MockWebServer require
// a Thread.sleep or some other delay to allow for subsequent states.
@Suppress("ControlFlowWithEmptyBody", "EmptyWhileBlock")
while (capture.values.size < expected) {}
capture.block()
} finally {
removeObserver(observer)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment