From 321803fe877e6d8d2dd04a72337f265b95cfda22 Mon Sep 17 00:00:00 2001 From: emmano Date: Wed, 15 Sep 2021 19:48:03 -0500 Subject: [PATCH] coroutine-flow-support: Add test for FlowRuntime and FlowStream. Add TestFlow as a counterpart to TestObserver. (EO) --- dependencies.gradle | 2 +- formula-coroutines/build.gradle | 1 + .../formula/coroutines/FlowFormula.kt | 18 ++++++ .../formula/coroutines}/CoroutineTest.kt | 2 +- .../formula/coroutines}/CoroutineTestRule.kt | 2 +- .../formula/coroutines/FlowFormulaTest.kt | 61 +++++++++++++++++++ formula-test/build.gradle | 3 + .../com/instacart/formula/test/TestFlow.kt | 47 ++++++++++++++ .../instacart/formula/test/TestFormulaFlow.kt | 60 ++++++++++++++++++ .../formula/test/TestRuntimeExtensions.kt | 25 ++++++++ samples/stopwatch-coroutines/build.gradle | 1 - .../formula/stopwatch/StopwatchFormula.kt | 16 ++--- .../formula/stopwatch/StopwatchFormulaTest.kt | 33 ---------- 13 files changed, 227 insertions(+), 44 deletions(-) create mode 100644 formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowFormula.kt rename {samples/stopwatch-coroutines/src/test/java/com/instacart/formula/stopwatch => formula-coroutines/src/test/java/com/instacart/formula/coroutines}/CoroutineTest.kt (92%) rename {samples/stopwatch-coroutines/src/test/java/com/instacart/formula/stopwatch => formula-coroutines/src/test/java/com/instacart/formula/coroutines}/CoroutineTestRule.kt (95%) create mode 100644 formula-coroutines/src/test/java/com/instacart/formula/coroutines/FlowFormulaTest.kt create mode 100644 formula-test/src/main/java/com/instacart/formula/test/TestFlow.kt create mode 100644 formula-test/src/main/java/com/instacart/formula/test/TestFormulaFlow.kt delete mode 100644 samples/stopwatch-coroutines/src/test/java/com/instacart/formula/stopwatch/StopwatchFormulaTest.kt diff --git a/dependencies.gradle b/dependencies.gradle index ed957ef06..a0f3c66ce 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -72,7 +72,7 @@ ext { rxandroid : "io.reactivex.rxjava3:rxandroid:3.0.0", rxrelays : "com.jakewharton.rxrelay3:rxrelay:3.0.1", truth : "com.google.truth:truth:$truthVersion", - cororutinesTest : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion" + coroutinesTest : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion" ] } diff --git a/formula-coroutines/build.gradle b/formula-coroutines/build.gradle index 8c18ef51d..2143f8374 100644 --- a/formula-coroutines/build.gradle +++ b/formula-coroutines/build.gradle @@ -17,4 +17,5 @@ dependencies { testImplementation project(":formula-test") testImplementation libraries.truth testImplementation libraries.junit + testImplementation libraries.coroutinesTest } \ No newline at end of file diff --git a/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowFormula.kt b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowFormula.kt new file mode 100644 index 000000000..c58ea6c07 --- /dev/null +++ b/formula-coroutines/src/main/java/com/instacart/formula/coroutines/FlowFormula.kt @@ -0,0 +1,18 @@ +package com.instacart.formula.coroutines + +import com.instacart.formula.Stream +import com.instacart.formula.StreamFormula +import kotlinx.coroutines.flow.Flow + +abstract class FlowFormula : StreamFormula() { + + abstract override fun initialValue(input: Input): Output + + abstract fun flow(input: Input): Flow + + final override fun stream(input: Input): Stream { + return FlowStream.fromFlow { + flow(input) + } + } +} \ No newline at end of file diff --git a/samples/stopwatch-coroutines/src/test/java/com/instacart/formula/stopwatch/CoroutineTest.kt b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTest.kt similarity index 92% rename from samples/stopwatch-coroutines/src/test/java/com/instacart/formula/stopwatch/CoroutineTest.kt rename to formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTest.kt index 0292d470c..ec471efe7 100644 --- a/samples/stopwatch-coroutines/src/test/java/com/instacart/formula/stopwatch/CoroutineTest.kt +++ b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTest.kt @@ -1,4 +1,4 @@ -package com.instacart.formula.stopwatch +package com.instacart.formula.coroutines import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineScope diff --git a/samples/stopwatch-coroutines/src/test/java/com/instacart/formula/stopwatch/CoroutineTestRule.kt b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTestRule.kt similarity index 95% rename from samples/stopwatch-coroutines/src/test/java/com/instacart/formula/stopwatch/CoroutineTestRule.kt rename to formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTestRule.kt index 84d9e2013..3965def0f 100644 --- a/samples/stopwatch-coroutines/src/test/java/com/instacart/formula/stopwatch/CoroutineTestRule.kt +++ b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/CoroutineTestRule.kt @@ -1,4 +1,4 @@ -package com.instacart.formula.stopwatch +package com.instacart.formula.coroutines import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers diff --git a/formula-coroutines/src/test/java/com/instacart/formula/coroutines/FlowFormulaTest.kt b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/FlowFormulaTest.kt new file mode 100644 index 000000000..aed58020f --- /dev/null +++ b/formula-coroutines/src/test/java/com/instacart/formula/coroutines/FlowFormulaTest.kt @@ -0,0 +1,61 @@ +package com.instacart.formula.coroutines + +import com.google.common.truth.Truth +import com.instacart.formula.test.test +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.* +import org.junit.Rule +import org.junit.Test + +@ExperimentalStdlibApi +class FlowFormulaTest : CoroutineTest { + + @get:Rule + override val coroutineRule = CoroutineTestRule() + + @Test + fun `initial value`() = test { + TestFlowFormula() + .test("initial", this) + .apply { + Truth.assertThat(values()).containsExactly(0).inOrder() + } + } + + @Test + fun `initial value and subsequent events from relay`() = test { + TestFlowFormula() + .test("initial", this) + .apply { + formula.sharedFlow.tryEmit(1) + formula.sharedFlow.tryEmit(2) + formula.sharedFlow.tryEmit(3) + } + .apply { + Truth.assertThat(values()).containsExactly(0, 1, 2, 3).inOrder() + } + } + + @Test + fun `new input restarts formula`() = test { + TestFlowFormula() + .test(this) + .input("initial") + .apply { formula.sharedFlow.tryEmit(1) } + .input("reset") + .apply { formula.sharedFlow.tryEmit(1) } + .apply { + Truth.assertThat(values()).containsExactly(0, 1, 0, 1).inOrder() + } + } + + internal class TestFlowFormula : FlowFormula() { + + val sharedFlow = + MutableSharedFlow(0, extraBufferCapacity = 1, BufferOverflow.DROP_OLDEST) + + override fun initialValue(input: String): Int = 0 + + override fun flow(input: String): Flow = sharedFlow.asSharedFlow() + } +} diff --git a/formula-test/build.gradle b/formula-test/build.gradle index 0dda0fe04..8c8d8e8c3 100644 --- a/formula-test/build.gradle +++ b/formula-test/build.gradle @@ -18,7 +18,10 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { dependencies { implementation libraries.kotlin + implementation libraries.coroutines + api project(":formula-rxjava3") + api project(":formula-coroutines") testImplementation libraries.truth testImplementation libraries.junit diff --git a/formula-test/src/main/java/com/instacart/formula/test/TestFlow.kt b/formula-test/src/main/java/com/instacart/formula/test/TestFlow.kt new file mode 100644 index 000000000..fd81d4c7d --- /dev/null +++ b/formula-test/src/main/java/com/instacart/formula/test/TestFlow.kt @@ -0,0 +1,47 @@ +package com.instacart.formula.test + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn + +class TestFlow(private val upstream: Flow) : Flow by upstream { + private val values = mutableListOf() + private val errors = mutableListOf() + + fun values(): List = values.toList() + + fun assertNoErrors() = run { + check(errors.isEmpty()) { "There are ${errors.size} errors" } + this + } + + private val internalFlow by lazy { + callbackFlow { + upstream + .catch { + errors.add(it) + } + .collect { + values.add(it) + trySend(it) + } + + awaitClose { + cancel() + } + } + } + fun test() = internalFlow + +} + +fun Flow.test(scope: CoroutineScope) : TestFlow { + val testFlow = TestFlow(this) + testFlow.test().launchIn(scope) + return testFlow +} \ No newline at end of file diff --git a/formula-test/src/main/java/com/instacart/formula/test/TestFormulaFlow.kt b/formula-test/src/main/java/com/instacart/formula/test/TestFormulaFlow.kt new file mode 100644 index 000000000..2e1b46a50 --- /dev/null +++ b/formula-test/src/main/java/com/instacart/formula/test/TestFormulaFlow.kt @@ -0,0 +1,60 @@ +package com.instacart.formula.test + +import com.instacart.formula.IFormula +import com.instacart.formula.coroutines.toFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged + +class TestFormulaFlow>( + scope: CoroutineScope, + val formula: FormulaT +) { + + private var started: Boolean = false + private val inputFlow = + MutableSharedFlow(1) + private val testFlow = formula + .toFlow(inputFlow.distinctUntilChanged()) + .test(scope) + .assertNoErrors() + + fun values(): List { + return testFlow.values() + } + + /** + * Passes input to [formula]. + */ + fun input(value: Input) = apply { + started = true + assertNoErrors() // Check before interaction + inputFlow.tryEmit(value) + assertNoErrors() // Check after interaction + } + + inline fun output(assert: Output.() -> Unit) = apply { + ensureFormulaIsRunning() + assertNoErrors() // Check before interaction + assert(values().last()) + assertNoErrors() // Check after interaction + } + + fun assertOutputCount(count: Int) = apply { + ensureFormulaIsRunning() + assertNoErrors() + val size = values().size + assert(size == count) { + "Expected: $count, was: $size" + } + } + + fun assertNoErrors() = apply { + testFlow.assertNoErrors() + } + + @PublishedApi + internal fun ensureFormulaIsRunning() { + if (!started) throw IllegalStateException("Formula is not running. Call [TeatFormulaObserver.input] to start it.") + } +} diff --git a/formula-test/src/main/java/com/instacart/formula/test/TestRuntimeExtensions.kt b/formula-test/src/main/java/com/instacart/formula/test/TestRuntimeExtensions.kt index 2df9266b5..24fd933e9 100644 --- a/formula-test/src/main/java/com/instacart/formula/test/TestRuntimeExtensions.kt +++ b/formula-test/src/main/java/com/instacart/formula/test/TestRuntimeExtensions.kt @@ -2,6 +2,7 @@ package com.instacart.formula.test import com.instacart.formula.IFormula import com.instacart.formula.Stream +import kotlinx.coroutines.CoroutineScope /** * An extension function to create a [TestFormulaObserver] for a [IFormula] instance. @@ -12,6 +13,16 @@ fun > F.test(): TestFormul return TestFormulaObserver(this) } +/** + * An extension function to create a [TestFormulaFlow] for a [IFormula] instance. + * + * Note: Formula won't start until you pass it an [input][TestFormulaFlow.input]. + */ + +fun > F.test(scope: CoroutineScope): TestFormulaFlow { + return TestFormulaFlow(scope, this) +} + /** * An extension function to create a [TestFormulaObserver] for a [IFormula] instance. * @@ -25,4 +36,18 @@ fun > F.test( } } +/** + * An extension function to create a [TestFormulaFlow] for a [IFormula] instance. + * + * @param initialInput Input passed to [IFormula]. + */ +fun > F.test( + initialInput: Input, + scope: CoroutineScope +): TestFormulaFlow { + return test(scope).apply { + input(initialInput) + } +} + fun Stream.test() = TestStreamObserver(this) diff --git a/samples/stopwatch-coroutines/build.gradle b/samples/stopwatch-coroutines/build.gradle index a6ea0076a..04b403643 100644 --- a/samples/stopwatch-coroutines/build.gradle +++ b/samples/stopwatch-coroutines/build.gradle @@ -51,5 +51,4 @@ dependencies { testImplementation libraries.androidx.test.rules testImplementation libraries.androidx.test.runner - testImplementation libraries.cororutinesTest } diff --git a/samples/stopwatch-coroutines/src/main/java/com/instacart/formula/stopwatch/StopwatchFormula.kt b/samples/stopwatch-coroutines/src/main/java/com/instacart/formula/stopwatch/StopwatchFormula.kt index 4d9052ab2..0fd60e775 100644 --- a/samples/stopwatch-coroutines/src/main/java/com/instacart/formula/stopwatch/StopwatchFormula.kt +++ b/samples/stopwatch-coroutines/src/main/java/com/instacart/formula/stopwatch/StopwatchFormula.kt @@ -20,6 +20,7 @@ class StopwatchFormula : Formula