Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/smarttoolfactory/rxjava-style-livedata-and-flow-testobserver
TestObserver class for LiveData to test multiple values like ViewState such as loading, and result states or multiple post and setValues
https://github.com/smarttoolfactory/rxjava-style-livedata-and-flow-testobserver
android-unit-test android-unit-testing livedata unit-testing
Last synced: about 1 month ago
JSON representation
TestObserver class for LiveData to test multiple values like ViewState such as loading, and result states or multiple post and setValues
- Host: GitHub
- URL: https://github.com/smarttoolfactory/rxjava-style-livedata-and-flow-testobserver
- Owner: SmartToolFactory
- License: apache-2.0
- Created: 2020-08-11T18:49:41.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2020-08-26T17:08:24.000Z (over 4 years ago)
- Last Synced: 2024-11-11T00:38:13.345Z (3 months ago)
- Topics: android-unit-test, android-unit-testing, livedata, unit-testing
- Language: Kotlin
- Homepage:
- Size: 19.5 KB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# RxJava Style LiveData and Flow TestObserver
TestObserver class for LiveData to test multiple values like ViewState such as loading, and result states or multiple post and setValues## LiveData
### Implementation
```
class TestObserver(private val liveData: LiveData) : Observer {// init {
// liveData.observeForever(this)
// }private val testValues = mutableListOf()
override fun onChanged(t: T) {
if (t != null) testValues.add(t)
println("⏰ TestObserver onChanged() testValues $testValues")}
fun assertNoValues(): TestObserver {
if (testValues.isNotEmpty()) throw AssertionException("Assertion error with actual size ${testValues.size}")
return this
}fun assertValueCount(count: Int): TestObserver {
if (count < 0) throw AssertionException("Assert count cannot be smaller than zero")
if (count != testValues.size) throw AssertionException("Assertion error with expected $count while actual ${testValues.size}")
return this
}fun assertValues(vararg predicates: T): TestObserver {
predicates.forEach { predicate ->
testValues.forEach { testValue ->
if (predicate != testValue) throw Exception("Assertion error")
}
}
return this
}fun assertValues(predicate: List.() -> Boolean): TestObserver {
testValues.predicate()
return this
}fun values(predicate: List.() -> Unit): TestObserver {
testValues.predicate()
return this
}fun values(): List {
return testValues
}fun dispose() {
testValues.clear()
liveData.removeObserver(this)
}
}fun LiveData.test(): TestObserver {
val testObserver = TestObserver(this)
observeForever(testObserver)
return testObserver
}class AssertionException(message: String) : Exception(message) {
}
```## Usage
```
@Test
fun test() {// GIVEN
val myTestData = MutableLiveData()
val testObserver = myTestData.test()// WHEN
myTestData.value = 1
myTestData.value = 2
myTestData.value = 3
// THEN
testObserver.assertValues {
(this[0] == 1 && this[1] == 2 && this[2] == 3)}.assertValueCount(3)
// 🔥 Do not forget to dispose
testObserver.dispose()
}```
## Flow
### Implementation
```
class FlowTestObserver(
private val coroutineScope: CoroutineScope,
private val flow: Flow,
private val waitForDelay: Boolean = false
) {
private val testValues = mutableListOf()
private var error: Throwable? = nullprivate var isInitialized = false
private var isCompleted = false
private lateinit var job: Job
private suspend fun init() {
job = createJob(coroutineScope)// Wait this job after end of possible delays
// job.join()
}private suspend fun initialize() {
if (!isInitialized) {
if (waitForDelay) {
try {
withTimeout(Long.MAX_VALUE) {
job = createJob(this)
}
} catch (e: Exception) {
isCompleted = false
}
} else {
job = createJob(coroutineScope)
}
}
}private fun createJob(scope: CoroutineScope): Job {
val job = flow
.onStart { isInitialized = true }
.onCompletion { cause ->
isCompleted = (cause == null)
}
.catch { throwable ->
error = throwable
}
.onEach { testValues.add(it) }
.launchIn(scope)
return job
}suspend fun assertNoValue(): FlowTestObserver {
initialize()
if (testValues.isNotEmpty()) throw AssertionError(
"Assertion error! Actual size ${testValues.size}"
)
return this
}suspend fun assertValueCount(count: Int): FlowTestObserver {
initialize()
if (count < 0) throw AssertionError(
"Assertion error! Value count cannot be smaller than zero"
)
if (count != testValues.size) throw AssertionError(
"Assertion error! Expected $count while actual ${testValues.size}"
)
return this
}suspend fun assertValues(vararg values: T): FlowTestObserver {
initialize()
if (!testValues.containsAll(values.asList()))
throw AssertionError("Assertion error! At least one value does not match")
return this
}suspend fun assertValues(predicate: (List) -> Boolean): FlowTestObserver {
initialize()
if (!predicate(testValues))
throw AssertionError("Assertion error! At least one value does not match")
return this
}/**
* Asserts that this [FlowTestObserver] received exactly one [Flow.onEach] or [Flow.collect]
* value for which the provided predicate returns `true`.
*/
suspend fun assertValue(predicate: (T) -> Boolean): FlowTestObserver {
return assertValueAt(0, predicate)
}suspend fun assertValueAt(index: Int, predicate: (T) -> Boolean): FlowTestObserver {
initialize()
if (testValues.size == 0) throw AssertionError("Assertion error! No values")
if (index < 0) throw AssertionError(
"Assertion error! Index cannot be smaller than zero"
)if (index > testValues.size) throw AssertionError(
"Assertion error! Invalid index: $index"
)if (!predicate(testValues[index]))
throw AssertionError("Assertion error! At least one value does not match")return this
}suspend fun assertValueAt(index: Int, value: T): FlowTestObserver {
initialize()
if (testValues.size == 0) throw AssertionError("Assertion error! No values")
if (index < 0) throw AssertionError(
"Assertion error! Index cannot be smaller than zero"
)if (index > testValues.size) throw AssertionError(
"Assertion error! Invalid index: $index"
)if (testValues[index] != value)
throw AssertionError("Assertion Error Objects don't match")return this
}/**
* Asserts that this [FlowTestObserver] received
* [Flow.catch] the exact same throwable. Since most exceptions don't implement `equals`
* it would be better to call overload to test against the class of
* an error instead of an instance of an error
*/
suspend fun assertError(throwable: Throwable): FlowTestObserver {initialize()
val errorNotNull = exceptionNotNull()
if (!(
errorNotNull::class.java == throwable::class.java &&
errorNotNull.message == throwable.message
)
)
throw AssertionError(
"Assertion Error! " +
"throwable: $throwable does not match $errorNotNull"
)
return this
}/**
* Asserts that this [FlowTestObserver] received
* [Flow.catch] which is an instance of the specified errorClass Class.
*/
suspend fun assertError(errorClass: Class): FlowTestObserver {initialize()
val errorNotNull = exceptionNotNull()
if (errorNotNull::class.java != errorClass)
throw AssertionError(
"Assertion Error! errorClass $errorClass" +
" does not match ${errorNotNull::class.java}"
)
return this
}/**
* Asserts that this [FlowTestObserver] received exactly [Flow.catch] event for which
* the provided predicate returns `true`.
*/
suspend fun assertError(predicate: (Throwable) -> Boolean): FlowTestObserver {initialize()
val errorNotNull = exceptionNotNull()
if (!predicate(errorNotNull))
throw AssertionError("Assertion Error! Exception for $errorNotNull")
return this
}suspend fun assertNoErrors(): FlowTestObserver {
initialize()
if (error != null)
throw AssertionError("Assertion Error! Exception occurred $error")return this
}suspend fun assertNull(): FlowTestObserver {
initialize()
testValues.forEach {
if (it != null) throw AssertionError(
"Assertion Error! " +
"There are more than one item that is not null"
)
}return this
}/**
* Assert that this [FlowTestObserver] received [Flow.onCompletion] event without a [Throwable]
*/
suspend fun assertComplete(): FlowTestObserver {initialize()
if (!isCompleted) throw AssertionError(
"Assertion Error!" +
" Job is not completed or onCompletion called with a error!"
)
return this
}/**
* Assert that this [FlowTestObserver] either not received [Flow.onCompletion] event or
* received event with
*/
suspend fun assertNotComplete(): FlowTestObserver {initialize()
if (isCompleted) throw AssertionError("Assertion Error! Job is completed!")
return this
}suspend fun values(predicate: (List) -> Unit): FlowTestObserver {
predicate(testValues)
return this
}suspend fun values(): List {
initialize()
return testValues
}private fun exceptionNotNull(): Throwable {
if (error == null)
throw AssertionError("There is no exception")return error!!
}fun dispose() {
job.cancel()
}
}/**
* Creates a RxJava2 style test observer that uses `onStart`, `onEach`, `onCompletion`
*
* * Set waitForDelay true for testing delay.
*
* ### Note: waiting for delay with a channel that sends values throw TimeoutCancellationException,
* don't use timeout with channel
* TODO Fix channel issue
*/
suspend fun Flow.test(
scope: CoroutineScope,
waitForDelay: Boolean = true
): FlowTestObserver {return FlowTestObserver(scope, this@test, waitForDelay)
}/**
* Test function that awaits with time out until each delay method is run and then since
* it takes a predicate that runs after a timeout.
*/
suspend fun Flow.testAfterDelay(
scope: CoroutineScope,
predicate: suspend FlowTestObserver.() -> Unit): Job {
return scope.launch(coroutineContext) {
FlowTestObserver(this, this@testAfterDelay, true).predicate()
}
}```
## Usage
```
postRemoteRepository.getPostFlow()
.test(testCoroutineScope)
// .assertError(Exception("Network Exception"))
.assertError {
it.message == "Network Exception"
}
.dispose()
testCoroutineScope.runBlockingTest {// GIVEN
every { postDBRepository.getPostListFlow() } returns flow { emit(listOf()) }// WHEN
val testObserver = postDBRepository.getPostListFlow().test(this)// THEN
val actual = testObserver.values()[0]
Truth.assertThat(actual.size).isEqualTo(0)
testObserver.dispose()
}