{"id":19521799,"url":"https://github.com/smarttoolfactory/rxjava-style-livedata-and-flow-testobserver","last_synced_at":"2025-09-05T13:38:52.446Z","repository":{"id":109487426,"uuid":"286823456","full_name":"SmartToolFactory/RxJava-Style-LiveData-And-Flow-TestObserver","owner":"SmartToolFactory","description":"TestObserver class for LiveData to test multiple values like ViewState such as loading, and result states or multiple post and setValues","archived":false,"fork":false,"pushed_at":"2020-08-26T17:08:24.000Z","size":20,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-08T14:11:54.478Z","etag":null,"topics":["android-unit-test","android-unit-testing","livedata","unit-testing"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SmartToolFactory.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-08-11T18:49:41.000Z","updated_at":"2020-10-03T15:21:31.000Z","dependencies_parsed_at":"2023-04-17T06:33:34.028Z","dependency_job_id":null,"html_url":"https://github.com/SmartToolFactory/RxJava-Style-LiveData-And-Flow-TestObserver","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FRxJava-Style-LiveData-And-Flow-TestObserver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FRxJava-Style-LiveData-And-Flow-TestObserver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FRxJava-Style-LiveData-And-Flow-TestObserver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FRxJava-Style-LiveData-And-Flow-TestObserver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SmartToolFactory","download_url":"https://codeload.github.com/SmartToolFactory/RxJava-Style-LiveData-And-Flow-TestObserver/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240771907,"owners_count":19854982,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android-unit-test","android-unit-testing","livedata","unit-testing"],"created_at":"2024-11-11T00:34:59.665Z","updated_at":"2025-02-26T00:46:13.235Z","avatar_url":"https://github.com/SmartToolFactory.png","language":"Kotlin","readme":"# RxJava Style LiveData and Flow TestObserver\nTestObserver class for LiveData to test multiple values like ViewState such as loading, and result states or multiple post and setValues\n\n## LiveData\n\n### Implementation\n\n```\nclass TestObserver\u003cT\u003e(private val liveData: LiveData\u003cT\u003e) : Observer\u003cT\u003e {\n\n//    init {\n//        liveData.observeForever(this)\n//    }\n\n    private val testValues = mutableListOf\u003cT\u003e()\n\n    override fun onChanged(t: T) {\n\n        if (t != null) testValues.add(t)\n        println(\"⏰ TestObserver onChanged()  testValues $testValues\")\n\n    }\n\n    fun assertNoValues(): TestObserver\u003cT\u003e {\n        if (testValues.isNotEmpty()) throw AssertionException(\"Assertion error with actual size ${testValues.size}\")\n        return this\n    }\n\n    fun assertValueCount(count: Int): TestObserver\u003cT\u003e {\n        if (count \u003c 0) throw AssertionException(\"Assert count cannot be smaller than zero\")\n        if (count != testValues.size) throw AssertionException(\"Assertion error with expected $count while actual ${testValues.size}\")\n        return this\n    }\n\n    fun assertValues(vararg predicates: T): TestObserver\u003cT\u003e {\n        predicates.forEach { predicate -\u003e\n            testValues.forEach { testValue -\u003e\n                if (predicate != testValue) throw  Exception(\"Assertion error\")\n            }\n        }\n        return this\n    }\n\n    fun assertValues(predicate: List\u003cT\u003e.() -\u003e Boolean): TestObserver\u003cT\u003e {\n        testValues.predicate()\n        return this\n    }\n\n    fun values(predicate: List\u003cT\u003e.() -\u003e Unit): TestObserver\u003cT\u003e {\n        testValues.predicate()\n        return this\n    }\n\n    fun values(): List\u003cT\u003e {\n        return testValues\n    }\n\n\n    fun dispose() {\n        testValues.clear()\n        liveData.removeObserver(this)\n    }\n}\n\n\nfun \u003cT\u003e LiveData\u003cT\u003e.test(): TestObserver\u003cT\u003e {\n\n    val testObserver = TestObserver(this)\n\n    observeForever(testObserver)\n\n    return testObserver\n}\n\nclass AssertionException(message: String) : Exception(message) {\n\n}\n```\n\n## Usage\n\n```\n@Test\nfun test() {\n\n    // GIVEN\n    val myTestData = MutableLiveData\u003cInt\u003e()\n    val testObserver = myTestData.test()\n\n    // WHEN\n    myTestData.value = 1\n    myTestData.value = 2\n    myTestData.value = 3\n    \n    // THEN\n    testObserver.assertValues {\n        (this[0] == 1 \u0026\u0026 this[1] == 2 \u0026\u0026 this[2] == 3)\n\n    }.assertValueCount(3)\n\n    // 🔥 Do not forget to dispose\n    testObserver.dispose()\n}\n\n```\n\n## Flow\n\n### Implementation\n\n```\n\nclass FlowTestObserver\u003cT\u003e(\n    private val coroutineScope: CoroutineScope,\n    private val flow: Flow\u003cT\u003e,\n    private val waitForDelay: Boolean = false\n) {\n    private val testValues = mutableListOf\u003cT\u003e()\n    private var error: Throwable? = null\n\n    private var isInitialized = false\n\n    private var isCompleted = false\n\n    private lateinit var job: Job\n\n    private suspend fun init() {\n        job = createJob(coroutineScope)\n\n        // Wait this job after end of possible delays\n//        job.join()\n    }\n\n    private suspend fun initialize() {\n\n        if (!isInitialized) {\n\n            if (waitForDelay) {\n                try {\n                    withTimeout(Long.MAX_VALUE) {\n                        job = createJob(this)\n                    }\n                } catch (e: Exception) {\n                    isCompleted = false\n                }\n            } else {\n                job = createJob(coroutineScope)\n            }\n        }\n    }\n\n    private fun createJob(scope: CoroutineScope): Job {\n\n        val job = flow\n            .onStart { isInitialized = true }\n            .onCompletion { cause -\u003e\n                isCompleted = (cause == null)\n            }\n            .catch { throwable -\u003e\n                error = throwable\n            }\n            .onEach { testValues.add(it) }\n            .launchIn(scope)\n        return job\n    }\n\n    suspend fun assertNoValue(): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (testValues.isNotEmpty()) throw AssertionError(\n            \"Assertion error! Actual size ${testValues.size}\"\n        )\n        return this\n    }\n\n    suspend fun assertValueCount(count: Int): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (count \u003c 0) throw AssertionError(\n            \"Assertion error! Value count cannot be smaller than zero\"\n        )\n        if (count != testValues.size) throw AssertionError(\n            \"Assertion error! Expected $count while actual ${testValues.size}\"\n        )\n        return this\n    }\n\n    suspend fun assertValues(vararg values: T): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (!testValues.containsAll(values.asList()))\n            throw AssertionError(\"Assertion error! At least one value does not match\")\n        return this\n    }\n\n    suspend fun assertValues(predicate: (List\u003cT\u003e) -\u003e Boolean): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (!predicate(testValues))\n            throw AssertionError(\"Assertion error! At least one value does not match\")\n        return this\n    }\n\n    /**\n     * Asserts that this [FlowTestObserver] received exactly one [Flow.onEach] or [Flow.collect]\n     * value for which the provided predicate returns `true`.\n     */\n    suspend fun assertValue(predicate: (T) -\u003e Boolean): FlowTestObserver\u003cT\u003e {\n        return assertValueAt(0, predicate)\n    }\n\n    suspend fun assertValueAt(index: Int, predicate: (T) -\u003e Boolean): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (testValues.size == 0) throw AssertionError(\"Assertion error! No values\")\n\n        if (index \u003c 0) throw AssertionError(\n            \"Assertion error! Index cannot be smaller than zero\"\n        )\n\n        if (index \u003e testValues.size) throw AssertionError(\n            \"Assertion error! Invalid index: $index\"\n        )\n\n        if (!predicate(testValues[index]))\n            throw AssertionError(\"Assertion error! At least one value does not match\")\n\n        return this\n    }\n\n    suspend fun assertValueAt(index: Int, value: T): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (testValues.size == 0) throw AssertionError(\"Assertion error! No values\")\n\n        if (index \u003c 0) throw AssertionError(\n            \"Assertion error! Index cannot be smaller than zero\"\n        )\n\n        if (index \u003e testValues.size) throw AssertionError(\n            \"Assertion error! Invalid index: $index\"\n        )\n\n        if (testValues[index] != value)\n            throw AssertionError(\"Assertion Error Objects don't match\")\n\n        return this\n    }\n\n    /**\n     * Asserts that this [FlowTestObserver] received\n     * [Flow.catch] the exact same throwable. Since most exceptions don't implement `equals`\n     * it would be better to call overload to test against the class of\n     * an error instead of an instance of an error\n     */\n    suspend fun assertError(throwable: Throwable): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        val errorNotNull = exceptionNotNull()\n\n        if (!(\n                    errorNotNull::class.java == throwable::class.java \u0026\u0026\n                            errorNotNull.message == throwable.message\n                    )\n        )\n            throw AssertionError(\n                \"Assertion Error! \" +\n                        \"throwable: $throwable does not match $errorNotNull\"\n            )\n        return this\n    }\n\n    /**\n     * Asserts that this [FlowTestObserver] received\n     * [Flow.catch] which is an instance of the specified errorClass Class.\n     */\n    suspend fun assertError(errorClass: Class\u003cout Throwable\u003e): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        val errorNotNull = exceptionNotNull()\n\n        if (errorNotNull::class.java != errorClass)\n            throw AssertionError(\n                \"Assertion Error! errorClass $errorClass\" +\n                        \" does not match ${errorNotNull::class.java}\"\n            )\n        return this\n    }\n\n    /**\n     *  Asserts that this [FlowTestObserver] received exactly [Flow.catch]  event for which\n     * the provided predicate returns `true`.\n     */\n    suspend fun assertError(predicate: (Throwable) -\u003e Boolean): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        val errorNotNull = exceptionNotNull()\n\n        if (!predicate(errorNotNull))\n            throw AssertionError(\"Assertion Error! Exception for $errorNotNull\")\n        return this\n    }\n\n    suspend fun assertNoErrors(): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (error != null)\n            throw AssertionError(\"Assertion Error! Exception occurred $error\")\n\n        return this\n    }\n\n    suspend fun assertNull(): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        testValues.forEach {\n            if (it != null) throw AssertionError(\n                \"Assertion Error! \" +\n                        \"There are more than one item that is not null\"\n            )\n        }\n\n        return this\n    }\n\n    /**\n     * Assert that this [FlowTestObserver] received [Flow.onCompletion] event without a [Throwable]\n     */\n    suspend fun assertComplete(): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (!isCompleted) throw AssertionError(\n            \"Assertion Error!\" +\n                    \" Job is not completed or onCompletion called with a error!\"\n        )\n        return this\n    }\n\n    /**\n     * Assert that this [FlowTestObserver] either not received [Flow.onCompletion] event or\n     * received event with\n     */\n    suspend fun assertNotComplete(): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (isCompleted) throw AssertionError(\"Assertion Error! Job is completed!\")\n        return this\n    }\n\n    suspend fun values(predicate: (List\u003cT\u003e) -\u003e Unit): FlowTestObserver\u003cT\u003e {\n        predicate(testValues)\n        return this\n    }\n\n    suspend fun values(): List\u003cT\u003e {\n\n        initialize()\n\n        return testValues\n    }\n\n    private fun exceptionNotNull(): Throwable {\n\n        if (error == null)\n            throw AssertionError(\"There is no exception\")\n\n        return error!!\n    }\n\n    fun dispose() {\n        job.cancel()\n    }\n}\n\n/**\n * Creates a RxJava2 style test observer that uses `onStart`, `onEach`, `onCompletion`\n *\n * * Set waitForDelay true for testing delay.\n *\n * ###  Note: waiting for delay with a channel that sends values throw TimeoutCancellationException,\n * don't use timeout with channel\n * TODO Fix channel issue\n */\nsuspend fun \u003cT\u003e Flow\u003cT\u003e.test(\n    scope: CoroutineScope,\n    waitForDelay: Boolean = true\n): FlowTestObserver\u003cT\u003e {\n\n    return FlowTestObserver(scope, this@test, waitForDelay)\n}\n\n/**\n * Test function that awaits with time out until each delay method is run and then since\n * it takes a predicate that runs after a timeout.\n */\nsuspend fun \u003cT\u003e Flow\u003cT\u003e.testAfterDelay(\n    scope: CoroutineScope,\n    predicate: suspend FlowTestObserver\u003cT\u003e.() -\u003e Unit\n\n): Job {\n    return scope.launch(coroutineContext) {\n        FlowTestObserver(this, this@testAfterDelay, true).predicate()\n    }\n}\n\n```\n\n## Usage\n```\n            postRemoteRepository.getPostFlow()\n                .test(testCoroutineScope)\n//                .assertError(Exception(\"Network Exception\"))\n                .assertError {\n                    it.message == \"Network Exception\"\n                }\n                .dispose()\n                \n                \n         testCoroutineScope.runBlockingTest {\n\n            // GIVEN\n            every { postDBRepository.getPostListFlow() } returns flow { emit(listOf()) }\n\n            // WHEN\n            val testObserver = postDBRepository.getPostListFlow().test(this)\n\n            // THEN\n            val actual = testObserver.values()[0]\n            Truth.assertThat(actual.size).isEqualTo(0)\n            testObserver.dispose()\n        }\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmarttoolfactory%2Frxjava-style-livedata-and-flow-testobserver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmarttoolfactory%2Frxjava-style-livedata-and-flow-testobserver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmarttoolfactory%2Frxjava-style-livedata-and-flow-testobserver/lists"}