diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f0c7c40f7..7f3d46343 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ ktlint = "0.39.0" kover = "0.9.0-RC" store = "5.1.0-SNAPSHOT" truth = "1.1.3" +turbine = "1.2.0" binary-compatibility-validator = "0.15.0-Beta.2" [libraries] @@ -56,7 +57,7 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor junit = { group = "junit", name = "junit", version.ref = "junit" } google-truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } touchlab-kermit = { group = "co.touchlab", name = "kermit", version.ref = "kermit" } -turbine = "app.cash.turbine:turbine:1.2.0" +turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } binary-compatibility-validator = {module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "binary-compatibility-validator"} [plugins] diff --git a/store/build.gradle.kts b/store/build.gradle.kts index 790ca9373..3b928ce89 100644 --- a/store/build.gradle.kts +++ b/store/build.gradle.kts @@ -26,6 +26,7 @@ kotlin { dependencies { implementation(libs.junit) implementation(libs.kotlinx.coroutines.test) + implementation(libs.turbine) } } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt index 4f009f849..bc1ad5037 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FetcherResponseTests.kt @@ -1,5 +1,6 @@ package org.mobilenativefoundation.store.store5 +import app.cash.turbine.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.flowOf @@ -7,7 +8,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest -import org.mobilenativefoundation.store.store5.util.assertEmitsExactly import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -34,9 +34,9 @@ class FetcherResponseTests { } @Test - fun givenAFetcherThatEmitsErrorAndDataWhenSteamingThenItCanEmitValueAfterAnError() { - val exception = RuntimeException("first error") + fun givenAFetcherThatEmitsErrorAndDataWhenSteamingThenItCanEmitValueAfterAnError() = testScope.runTest { + val exception = RuntimeException("first error") val store = StoreBuilder.from( fetcher = @@ -48,18 +48,23 @@ class FetcherResponseTests { }, ).buildWithTestScope() - assertEmitsExactly( - store.stream( - StoreReadRequest.fresh(1), - ), - listOf( + store.stream(StoreReadRequest.fresh(1)).test { + assertEquals( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Error.Exception(exception, StoreReadResponseOrigin.Fetcher()), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data("1", StoreReadResponseOrigin.Fetcher()), - ), - ) + awaitItem(), + ) + } } - } @Test fun givenTransformerWhenRawValueThenUnwrappedValueReturnedAndValueIsCached() = @@ -69,27 +74,32 @@ class FetcherResponseTests { StoreBuilder .from(fetcher).buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = 9, origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( + awaitItem(), + ) + } + + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( StoreReadResponse.Data( value = 9, origin = StoreReadResponseOrigin.Cache, ), - ), - ) + awaitItem(), + ) + } } @Test @@ -110,30 +120,39 @@ class FetcherResponseTests { StoreBuilder.from(fetcher) .buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Error.Message( message = "zero", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( + awaitItem(), + ) + } + + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = 1, origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -156,30 +175,39 @@ class FetcherResponseTests { .from(fetcher) .buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Error.Exception( error = e, origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( + awaitItem(), + ) + } + + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = 1, origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -200,36 +228,46 @@ class FetcherResponseTests { .from(fetcher = fetcher) .buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Error.Exception( error = e, origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( + awaitItem(), + ) + } + + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = 1, origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test fun givenAFetcherThatEmitsCustomErrorWhenStreamingThenCustomErrorShouldBeEmitted() = testScope.runTest { data class TestCustomError(val errorMessage: String) + val customError = TestCustomError("Test custom error") val store = @@ -242,16 +280,20 @@ class FetcherResponseTests { }, ).buildWithTestScope() - assertEmitsExactly( - store.stream(StoreReadRequest.fresh(1)), - listOf( + store.stream(StoreReadRequest.fresh(1)).test { + assertEquals( StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Error.Custom( error = customError, origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } private fun StoreBuilder.buildWithTestScope() = scope(testScope).build() diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt index af2a2f2d7..f377ef45e 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/FlowStoreTests.kt @@ -15,6 +15,7 @@ */ package org.mobilenativefoundation.store.store5 +import app.cash.turbine.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.async @@ -39,7 +40,6 @@ import org.mobilenativefoundation.store.store5.util.FakeFlowingFetcher import org.mobilenativefoundation.store.store5.util.InMemoryPersister import org.mobilenativefoundation.store.store5.util.asFlowable import org.mobilenativefoundation.store.store5.util.asSourceOfTruth -import org.mobilenativefoundation.store.store5.util.assertEmitsExactly import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals @@ -124,62 +124,79 @@ class FlowStoreTests { sourceOfTruth = persister.asSourceOfTruth(), ).buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Cache, ), - // note that we still get the data from persister as well as we don't listen to - // the persister for the cached items unless there is an active stream, which - // means cache can go out of sync w/ the persister + awaitItem(), + ) + + // note that we still get the data from persister as well as we don't listen to + // the persister for the cached items unless there is an active stream, which + // means cache can go out of sync w/ the persister + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.SourceOfTruth, ), - ), - ) + awaitItem(), + ) + } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.Cache, ), + awaitItem(), + ) + + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.SourceOfTruth, ), - ), - ) + awaitItem(), + ) + } } @Test @@ -198,39 +215,55 @@ class FlowStoreTests { sourceOfTruth = persister.asSourceOfTruth(), ).buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Cache, ), + awaitItem(), + ) + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.SourceOfTruth, ), + awaitItem(), + ) + + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -245,35 +278,47 @@ class FlowStoreTests { StoreBuilder.from(fetcher = fetcher) .buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Cache, ), + awaitItem(), + ) + + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -288,31 +333,39 @@ class FlowStoreTests { StoreBuilder.from(fetcher = fetcher) .buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)), - listOf( + pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)), - listOf( + pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -333,42 +386,63 @@ class FlowStoreTests { .disableCache() .buildWithTestScope() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + awaitItem(), + ) + } + + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.SourceOfTruth, ), + awaitItem(), + ) + + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -392,22 +466,31 @@ class FlowStoreTests { delay(10) persister.flowWriter(3, "local-1") } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "local-1", origin = StoreReadResponseOrigin.SourceOfTruth, ), + awaitItem(), + ) + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -436,30 +519,47 @@ class FlowStoreTests { delay(10) // go in between two server requests persister.flowWriter(3, "local-2") } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "local-1", origin = StoreReadResponseOrigin.SourceOfTruth, ), + awaitItem(), + ) + + assertEquals( Data( value = "three-1", origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "local-2", origin = StoreReadResponseOrigin.SourceOfTruth, ), + awaitItem(), + ) + + assertEquals( Data( value = "three-2", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -481,38 +581,56 @@ class FlowStoreTests { delay(10) persister.flowWriter(3, "local-1") } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)), - listOf( + + pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Error.Exception( error = exception, origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "local-1", origin = StoreReadResponseOrigin.SourceOfTruth, ), - ), - ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)), - listOf( + awaitItem(), + ) + } + + pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)).test { + assertEquals( Data( value = "local-1", origin = StoreReadResponseOrigin.SourceOfTruth, ), + awaitItem(), + ) + + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Error.Exception( error = exception, origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -530,25 +648,37 @@ class FlowStoreTests { val firstFetch = pipeline.fresh(3) // prime the cache assertEquals("local-1", firstFetch) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.NoNewData( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "local-1", origin = StoreReadResponseOrigin.Cache, ), + awaitItem(), + ) + + assertEquals( Data( value = "local-1", origin = StoreReadResponseOrigin.SourceOfTruth, ), - ), - ) + awaitItem(), + ) + } } @Test @@ -566,25 +696,37 @@ class FlowStoreTests { val firstFetch = pipeline.fresh(3) // prime the cache assertEquals("local-1", firstFetch) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Data( value = "local-1", origin = StoreReadResponseOrigin.Cache, ), + awaitItem(), + ) + + assertEquals( Data( value = "local-1", origin = StoreReadResponseOrigin.SourceOfTruth, ), + awaitItem(), + ) + + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.NoNewData( origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -607,21 +749,29 @@ class FlowStoreTests { val firstFetch = pipeline.fresh(3) // prime the cache assertEquals("remote-1", firstFetch) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.NoNewData( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( Data( value = "remote-1", origin = StoreReadResponseOrigin.Cache, ), - ), - ) + awaitItem(), + ) + } } @Test @@ -644,21 +794,29 @@ class FlowStoreTests { val firstFetch = pipeline.fresh(3) // prime the cache assertEquals("remote-1", firstFetch) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Data( value = "remote-1", origin = StoreReadResponseOrigin.Cache, ), + awaitItem(), + ) + + assertEquals( Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.NoNewData( origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test @@ -791,7 +949,7 @@ class FlowStoreTests { } @Test - fun givenCacheAndNoSourceOfTruthWhen3CachedStreamsWithRefreshAnd1stHasSlowCollectionThen1stStreamsGets3FetchUpdatesAndOtherStreamsGetCacheResultAndFetchResult() = + fun testSlowFirstCollectorGetsAllFetchUpdatesOthersGetCacheAndLatestFetchResult() = testScope.runTest { val fetcher = FakeFetcher( @@ -821,23 +979,38 @@ class FlowStoreTests { fetcher1Collected, ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Data(origin = StoreReadResponseOrigin.Cache, value = "three-1"), + awaitItem(), + ) + assertEquals( Loading(origin = StoreReadResponseOrigin.Fetcher()), + awaitItem(), + ) + + assertEquals( Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-2"), - ), - ) + awaitItem(), + ) + } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Data(origin = StoreReadResponseOrigin.Cache, value = "three-2"), + awaitItem(), + ) + + assertEquals( Loading(origin = StoreReadResponseOrigin.Fetcher()), + awaitItem(), + ) + assertEquals( Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-3"), - ), - ) + awaitItem(), + ) + } + testScope.advanceUntilIdle() assertEquals( listOf( @@ -853,7 +1026,7 @@ class FlowStoreTests { } @Test - fun givenCacheAndNoSourceOfTruthWhen2CachedStreamsWithRefreshThenFirstStreamsGets2FetchUpdatesAnd2ndStreamGetsCacheResultAndFetchResult() = + fun testFirstStreamGetsTwoFetchUpdatesSecondGetsCacheAndFetchResult() = testScope.runTest { val fetcher = FakeFetcher( @@ -880,14 +1053,23 @@ class FlowStoreTests { fetcher1Collected, ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( Data(origin = StoreReadResponseOrigin.Cache, value = "three-1"), + awaitItem(), + ) + + assertEquals( Loading(origin = StoreReadResponseOrigin.Fetcher()), + awaitItem(), + ) + + assertEquals( Data(origin = StoreReadResponseOrigin.Fetcher(), value = "three-2"), - ), - ) + awaitItem(), + ) + } + testScope.runCurrent() assertEquals( listOf( diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/HotFlowStoreTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/HotFlowStoreTests.kt index fdc905e74..7d3c2e64f 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/HotFlowStoreTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/HotFlowStoreTests.kt @@ -1,12 +1,13 @@ package org.mobilenativefoundation.store.store5 +import app.cash.turbine.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest -import org.mobilenativefoundation.store.store5.util.assertEmitsExactly import kotlin.test.Test import kotlin.test.assertEquals @@ -29,42 +30,56 @@ class HotFlowStoreTests { .scope(testScope) .build() - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher(), - ), - StoreReadResponse.Data( - value = "three-1", - origin = StoreReadResponseOrigin.Fetcher(), - ), - ), - ) - assertEmitsExactly( - pipeline.stream( - StoreReadRequest.cached(3, refresh = false), - ), - listOf( - StoreReadResponse.Data( - value = "three-1", - origin = StoreReadResponseOrigin.Cache, - ), - ), - ) + val job = + launch { + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + awaitItem(), + ) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( - StoreReadResponse.Loading( - origin = StoreReadResponseOrigin.Fetcher(), - ), - StoreReadResponse.Data( - value = "three-2", - origin = StoreReadResponseOrigin.Fetcher(), - ), - ), - ) + assertEquals( + StoreReadResponse.Data( + value = "three-1", + origin = StoreReadResponseOrigin.Fetcher(), + ), + awaitItem(), + ) + } + + pipeline.stream( + StoreReadRequest.cached(3, refresh = false), + ).test { + assertEquals( + StoreReadResponse.Data( + value = "three-1", + origin = StoreReadResponseOrigin.Cache, + ), + awaitItem(), + ) + } + + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( + StoreReadResponse.Loading( + origin = StoreReadResponseOrigin.Fetcher(), + ), + awaitItem(), + ) + + assertEquals( + StoreReadResponse.Data( + value = "three-2", + origin = StoreReadResponseOrigin.Fetcher(), + ), + awaitItem(), + ) + } + } + + job.cancel() } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/MapIndexedTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/MapIndexedTests.kt index a1650e13d..da4527c70 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/MapIndexedTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/MapIndexedTests.kt @@ -1,20 +1,23 @@ package org.mobilenativefoundation.store.store5 -import kotlinx.coroutines.ExperimentalCoroutinesApi +import app.cash.turbine.test import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed -import org.mobilenativefoundation.store.store5.util.assertEmitsExactly import kotlin.test.Test +import kotlin.test.assertEquals -@OptIn(ExperimentalCoroutinesApi::class) class MapIndexedTests { private val scope = TestScope() @Test fun mapIndexed() = scope.runTest { - assertEmitsExactly(flowOf(5, 6).mapIndexed { index, value -> index to value }, listOf(0 to 5, 1 to 6)) + flowOf(5, 6).mapIndexed { index, value -> index to value }.test { + assertEquals(0 to 5, awaitItem()) + assertEquals(1 to 6, awaitItem()) + awaitComplete() + } } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt index 200b0ae2c..79808aaf6 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthErrorsTests.kt @@ -1,5 +1,6 @@ package org.mobilenativefoundation.store.store5 +import app.cash.turbine.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.cancelAndJoin @@ -15,8 +16,8 @@ import org.mobilenativefoundation.store.store5.SourceOfTruth.WriteException import org.mobilenativefoundation.store.store5.util.FakeFetcher import org.mobilenativefoundation.store.store5.util.InMemoryPersister import org.mobilenativefoundation.store.store5.util.asSourceOfTruth -import org.mobilenativefoundation.store.store5.util.assertEmitsExactly import kotlin.test.Test +import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) @FlowPreview @@ -44,10 +45,13 @@ class SourceOfTruthErrorsTests { throw TestException("i fail") } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Error.Exception( error = WriteException( @@ -57,8 +61,9 @@ class SourceOfTruthErrorsTests { ), origin = StoreReadResponseOrigin.SourceOfTruth, ), - ), - ) + awaitItem(), + ) + } } @Test @@ -83,9 +88,8 @@ class SourceOfTruthErrorsTests { throw TestException(value ?: "null") } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = false)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = false)).test { + assertEquals( StoreReadResponse.Error.Exception( error = ReadException( @@ -94,12 +98,20 @@ class SourceOfTruthErrorsTests { ), origin = StoreReadResponseOrigin.SourceOfTruth, ), - // after disk fails, we should still invoke fetcher + awaitItem(), + ) + + // after disk fails, we should still invoke fetcher + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), - // and after fetcher writes the value, it will trigger another read which will also - // fail + awaitItem(), + ) + + // and after fetcher writes the value, it will trigger another read which will also + // fail + assertEquals( StoreReadResponse.Error.Exception( error = ReadException( @@ -108,8 +120,9 @@ class SourceOfTruthErrorsTests { ), origin = StoreReadResponseOrigin.SourceOfTruth, ), - ), - ) + awaitItem(), + ) + } } @Test @@ -135,12 +148,14 @@ class SourceOfTruthErrorsTests { } value } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + assertEquals( StoreReadResponse.Error.Exception( error = WriteException( @@ -150,10 +165,18 @@ class SourceOfTruthErrorsTests { ), origin = StoreReadResponseOrigin.SourceOfTruth, ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = "b", origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Error.Exception( error = WriteException( @@ -163,18 +186,27 @@ class SourceOfTruthErrorsTests { ), origin = StoreReadResponseOrigin.SourceOfTruth, ), - // disk flow will restart after a failed write (because we stopped it before the - // write attempt starts, so we will get the disk value again). + awaitItem(), + ) + + // disk flow will restart after a failed write (because we stopped it before the + // write attempt starts, so we will get the disk value again). + assertEquals( StoreReadResponse.Data( value = "b", origin = StoreReadResponseOrigin.SourceOfTruth, ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = "d", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } // @Test @@ -279,24 +311,34 @@ class SourceOfTruthErrorsTests { // miss both failures but arrive before d is fetched delay(70) - assertEmitsExactly( - pipeline.stream(StoreReadRequest.skipMemory(3, refresh = true)), - listOf( + + pipeline.stream(StoreReadRequest.skipMemory(3, refresh = true)).test { + assertEquals( StoreReadResponse.Data( value = "b", origin = StoreReadResponseOrigin.SourceOfTruth, ), - // don't receive the write exception because technically it started before we - // started reading + awaitItem(), + ) + + // don't receive the write exception because technically it started before we + // started reading + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = "d", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } + collector.cancelAndJoin() } @@ -366,9 +408,8 @@ class SourceOfTruthErrorsTests { } value } - assertEmitsExactly( - pipeline.stream(StoreReadRequest.cached(3, refresh = true)), - listOf( + pipeline.stream(StoreReadRequest.cached(3, refresh = true)).test { + assertEquals( StoreReadResponse.Error.Exception( origin = StoreReadResponseOrigin.SourceOfTruth, error = @@ -377,15 +418,24 @@ class SourceOfTruthErrorsTests { cause = TestException("first read"), ), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = "a", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthWithBarrierTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthWithBarrierTests.kt index 1cbab5218..656f9b91c 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthWithBarrierTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/SourceOfTruthWithBarrierTests.kt @@ -15,6 +15,7 @@ */ package org.mobilenativefoundation.store.store5 +import app.cash.turbine.test import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -32,7 +33,6 @@ import org.mobilenativefoundation.store.store5.SourceOfTruth.WriteException import org.mobilenativefoundation.store.store5.impl.PersistentSourceOfTruth import org.mobilenativefoundation.store.store5.impl.SourceOfTruthWithBarrier import org.mobilenativefoundation.store.store5.util.InMemoryPersister -import org.mobilenativefoundation.store.store5.util.assertEmitsExactly import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -147,9 +147,9 @@ class SourceOfTruthWithBarrierTests { persister.postReadCallback = { key, value -> throw exception } - assertEmitsExactly( - source.reader(1, CompletableDeferred(Unit)), - listOf( + + source.reader(1, CompletableDeferred(Unit)).test { + assertEquals( StoreReadResponse.Error.Exception( origin = StoreReadResponseOrigin.SourceOfTruth, error = @@ -158,8 +158,9 @@ class SourceOfTruthWithBarrierTests { cause = exception, ), ), - ), - ) + awaitItem(), + ) + } } @Test diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StreamWithoutSourceOfTruthTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StreamWithoutSourceOfTruthTests.kt index 8610aa233..a9a81c1b3 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StreamWithoutSourceOfTruthTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StreamWithoutSourceOfTruthTests.kt @@ -1,5 +1,6 @@ package org.mobilenativefoundation.store.store5 +import app.cash.turbine.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.async @@ -9,7 +10,6 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.mobilenativefoundation.store.store5.util.FakeFetcher -import org.mobilenativefoundation.store.store5.util.assertEmitsExactly import kotlin.test.Test import kotlin.test.assertEquals @@ -37,18 +37,22 @@ class StreamWithoutSourceOfTruthTests { ).take(3).toList() } delay(1_000) // make sure the async block starts first - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = "three-2", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } assertEquals( listOf( @@ -88,18 +92,22 @@ class StreamWithoutSourceOfTruthTests { ).take(3).toList() } delay(1_000) // make sure the async block starts first - assertEmitsExactly( - pipeline.stream(StoreReadRequest.fresh(3)), - listOf( + pipeline.stream(StoreReadRequest.fresh(3)).test { + assertEquals( StoreReadResponse.Loading( origin = StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( value = "three-2", origin = StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } assertEquals( listOf( diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt index 72ae4270b..d7898fee9 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/UpdaterTests.kt @@ -1,5 +1,6 @@ package org.mobilenativefoundation.store.store5 +import app.cash.turbine.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow @@ -9,7 +10,6 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.mobilenativefoundation.store.core5.ExperimentalStoreApi import org.mobilenativefoundation.store.store5.impl.extensions.inHours -import org.mobilenativefoundation.store.store5.util.assertEmitsExactly import org.mobilenativefoundation.store.store5.util.fake.Notes import org.mobilenativefoundation.store.store5.util.fake.NotesApi import org.mobilenativefoundation.store.store5.util.fake.NotesBookkeeping @@ -82,18 +82,20 @@ class UpdaterTests { val stream = store.stream(readRequest) // Read is success - val expected = - listOf( + stream.test { + assertEquals( StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( OutputNote(NoteData.Single(Notes.One), ttl = ttl), StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), ) - assertEmitsExactly( - stream, - expected, - ) + } val newNote = Notes.One.copy(title = "New Title-1") val writeRequest = @@ -193,18 +195,20 @@ class UpdaterTests { val stream = store.stream(readRequest) // Fetch is success and validator is not used - val expected = - listOf( + stream.test { + assertEquals( StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()), + awaitItem(), + ) + + assertEquals( StoreReadResponse.Data( OutputNote(NoteData.Single(Notes.One), ttl = ttl), StoreReadResponseOrigin.Fetcher(), ), + awaitItem(), ) - assertEmitsExactly( - stream, - expected, - ) + } val cachedReadRequest = StoreReadRequest.cached(NotesKey.Single(Notes.One.id), refresh = false) @@ -215,16 +219,19 @@ class UpdaterTests { // So we do not emit value in cache or SOT // Instead we get latest from network even though refresh = false - assertEmitsExactly( - cachedStream, - listOf( + cachedStream.test { + assertEquals( StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher(name = null)), + awaitItem(), + ) + assertEquals( StoreReadResponse.Data( OutputNote(NoteData.Single(Notes.One), ttl = ttl), StoreReadResponseOrigin.Fetcher(), ), - ), - ) + awaitItem(), + ) + } } @Test diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ValueFetcherTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ValueFetcherTests.kt index 3e138597f..26af2fb1f 100644 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ValueFetcherTests.kt +++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/ValueFetcherTests.kt @@ -1,13 +1,14 @@ package org.mobilenativefoundation.store.store5 +import app.cash.turbine.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest -import org.mobilenativefoundation.store.store5.util.assertEmitsExactly import kotlin.test.Test +import kotlin.test.assertEquals @ExperimentalCoroutinesApi @FlowPreview @@ -18,7 +19,11 @@ class ValueFetcherTests { fun givenValueFetcherWhenInvokeThenResultIsWrapped() = testScope.runTest { val fetcher = Fetcher.ofFlow { flowOf(it * it) } - assertEmitsExactly(fetcher(3), listOf(FetcherResult.Data(value = 9))) + + fetcher(3).test { + assertEquals(FetcherResult.Data(value = 9), awaitItem()) + awaitComplete() + } } @Test @@ -31,10 +36,10 @@ class ValueFetcherTests { throw e } } - assertEmitsExactly( - fetcher(3), - listOf(FetcherResult.Error.Exception(e)), - ) + fetcher(3).test { + assertEquals(FetcherResult.Error.Exception(e), awaitItem()) + awaitComplete() + } } @Test @@ -42,10 +47,10 @@ class ValueFetcherTests { testScope.runTest { val fetcher = Fetcher.of { it * it } - assertEmitsExactly( - fetcher(3), - listOf(FetcherResult.Data(value = 9)), - ) + fetcher(3).test { + assertEquals(FetcherResult.Data(value = 9), awaitItem()) + awaitComplete() + } } @Test @@ -56,6 +61,9 @@ class ValueFetcherTests { Fetcher.of { throw e } - assertEmitsExactly(fetcher(3), listOf(FetcherResult.Error.Exception(e))) + fetcher(3).test { + assertEquals(FetcherResult.Error.Exception(e), awaitItem()) + awaitComplete() + } } } diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AssertEmitsExactly.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AssertEmitsExactly.kt deleted file mode 100644 index c6572c9dc..000000000 --- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/util/AssertEmitsExactly.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.mobilenativefoundation.store.store5.util - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.toList -import kotlin.test.assertEquals - -suspend inline fun assertEmitsExactly( - actual: Flow, - expected: List, -) { - val flow = actual.take(expected.size).toList() - assertEquals(expected, flow) -}