From 86f640d301ecd7512b214fa81cb7af95b4a14b14 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 1 Nov 2022 09:09:15 +0000 Subject: [PATCH] more tests around the directory reducer and helpers --- .../app/dapk/st/core/ActivityExtensions.kt | 8 +-- .../dapk/st/directory/DirectoryReducerTest.kt | 54 +++++++++++-------- .../app/dapk/st/directory/FakeEventSource.kt | 6 ++- .../{ReducerTestScope.kt => ReducerTest.kt} | 51 ++++++++++++++++-- 4 files changed, 89 insertions(+), 30 deletions(-) rename features/directory/src/test/kotlin/app/dapk/st/directory/{ReducerTestScope.kt => ReducerTest.kt} (53%) diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt index 0bcc85e..c43a033 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ActivityExtensions.kt @@ -27,6 +27,7 @@ inline fun ComponentActivity.state( noinline factory: () -> StateViewModel ): Lazy> { val factoryPromise = object : Factory { + @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { return when(modelClass) { StateViewModel::class.java -> factory() as T @@ -34,7 +35,7 @@ inline fun ComponentActivity.state( } } } - return FooViewModelLazy( + return KeyedViewModelLazy( key = S::class.java.canonicalName!!, StateViewModel::class, { viewModelStore }, @@ -42,12 +43,11 @@ inline fun ComponentActivity.state( ) as Lazy> } -class FooViewModelLazy @JvmOverloads constructor( +class KeyedViewModelLazy @JvmOverloads constructor( private val key: String, private val viewModelClass: KClass, private val storeProducer: () -> ViewModelStore, private val factoryProducer: () -> ViewModelProvider.Factory, - private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty } ) : Lazy { private var cached: VM? = null @@ -60,7 +60,7 @@ class FooViewModelLazy @JvmOverloads constructor( ViewModelProvider( store, factory, - extrasProducer() + CreationExtras.Empty ).get(key, viewModelClass.java).also { cached = it } diff --git a/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryReducerTest.kt b/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryReducerTest.kt index 5e77fd1..98d96fc 100644 --- a/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryReducerTest.kt +++ b/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryReducerTest.kt @@ -3,6 +3,7 @@ package app.dapk.st.directory import app.dapk.st.directory.state.* import app.dapk.st.engine.DirectoryItem import app.dapk.st.engine.UnreadCount +import app.dapk.state.ReducerFactory import fake.FakeChatEngine import fixture.aRoomOverview import io.mockk.mockk @@ -18,61 +19,71 @@ class DirectoryReducerTest { private val fakeShortcutHandler = FakeShortcutHandler() private val fakeChatEngine = FakeChatEngine() private val fakeJobBag = FakeJobBag() - private val fakeEventSource = FakeEventSource() - private val reducer = directoryReducer( - fakeChatEngine, - fakeShortcutHandler.instance, - fakeJobBag.instance, - fakeEventSource, - ) + private val runReducerTest = testReducer { fakeEventSource -> + directoryReducer( + fakeChatEngine, + fakeShortcutHandler.instance, + fakeJobBag.instance, + fakeEventSource, + ) + } @Test - fun `initial state is empty loading`() = runReducerTest(reducer) { + fun `initial state is empty loading`() = runReducerTest { assertInitialState(DirectoryScreenState.EmptyLoading) } @Test - fun `given directory content, when Visible, then updates shortcuts and dispatches room state`() = runReducerTest(reducer) { + fun `given directory content, when Visible, then updates shortcuts and dispatches room state`() = runReducerTest { fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(listOf(AN_OVERVIEW)) } fakeJobBag.instance.expect { it.add("sync", any()) } fakeChatEngine.givenDirectory().returns(flowOf(listOf(AN_OVERVIEW_STATE))) reduce(ComponentLifecycle.OnVisible) - assertNoStateChange() - assertDispatches(listOf(DirectoryStateChange.Content(listOf(AN_OVERVIEW_STATE)))) + assertOnlyDispatches(listOf(DirectoryStateChange.Content(listOf(AN_OVERVIEW_STATE)))) } @Test - fun `given no directory content, when Visible, then updates shortcuts and dispatches empty state`() = runReducerTest(reducer) { + fun `given no directory content, when Visible, then updates shortcuts and dispatches empty state`() = runReducerTest { fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(emptyList()) } fakeJobBag.instance.expect { it.add("sync", any()) } fakeChatEngine.givenDirectory().returns(flowOf(emptyList())) reduce(ComponentLifecycle.OnVisible) - assertNoStateChange() - assertDispatches(listOf(DirectoryStateChange.Empty)) + assertOnlyDispatches(listOf(DirectoryStateChange.Empty)) } @Test - fun `when Gone, then cancels sync job`() = runReducerTest(reducer) { + fun `when Gone, then cancels sync job`() = runReducerTest { fakeJobBag.instance.expect { it.cancel("sync") } reduce(ComponentLifecycle.OnGone) - assertNoStateChange() - assertNoDispatches() + assertNoChanges() } @Test - fun `given ScrollToTop, then emits Scroll event`() = runReducerTest(reducer) { + fun `when ScrollToTop, then emits Scroll event`() = runReducerTest { reduce(DirectorySideEffect.ScrollToTop) - assertNoStateChange() - assertNoDispatches() - fakeEventSource.assertEvents(listOf(DirectoryEvent.ScrollToTop)) + assertOnlyEvents(listOf(DirectoryEvent.ScrollToTop)) + } + + @Test + fun `when Content StateChange, then returns Content state`() = runReducerTest { + reduce(DirectoryStateChange.Content(listOf(AN_OVERVIEW_STATE))) + + assertOnlyStateChange(DirectoryScreenState.Content(listOf(AN_OVERVIEW_STATE))) + } + + @Test + fun `when Empty StateChange, then returns Empty state`() = runReducerTest { + reduce(DirectoryStateChange.Empty) + + assertOnlyStateChange(DirectoryScreenState.Empty) } } @@ -83,3 +94,4 @@ internal class FakeShortcutHandler { class FakeJobBag { val instance = mockk() } + diff --git a/features/directory/src/test/kotlin/app/dapk/st/directory/FakeEventSource.kt b/features/directory/src/test/kotlin/app/dapk/st/directory/FakeEventSource.kt index 5caa5f2..d94097b 100644 --- a/features/directory/src/test/kotlin/app/dapk/st/directory/FakeEventSource.kt +++ b/features/directory/src/test/kotlin/app/dapk/st/directory/FakeEventSource.kt @@ -4,7 +4,7 @@ import org.amshove.kluent.internal.assertEquals class FakeEventSource : (E) -> Unit { - val captures = mutableListOf() + private val captures = mutableListOf() override fun invoke(event: E) { captures.add(event) @@ -13,4 +13,8 @@ class FakeEventSource : (E) -> Unit { fun assertEvents(expected: List) { assertEquals(expected, captures) } + + fun assertNoEvents() { + assertEquals(emptyList(), captures) + } } \ No newline at end of file diff --git a/features/directory/src/test/kotlin/app/dapk/st/directory/ReducerTestScope.kt b/features/directory/src/test/kotlin/app/dapk/st/directory/ReducerTest.kt similarity index 53% rename from features/directory/src/test/kotlin/app/dapk/st/directory/ReducerTestScope.kt rename to features/directory/src/test/kotlin/app/dapk/st/directory/ReducerTest.kt index bdc9c5e..af53ed5 100644 --- a/features/directory/src/test/kotlin/app/dapk/st/directory/ReducerTestScope.kt +++ b/features/directory/src/test/kotlin/app/dapk/st/directory/ReducerTest.kt @@ -12,16 +12,31 @@ import org.amshove.kluent.shouldBeEqualTo import test.ExpectTest import test.ExpectTestScope -fun runReducerTest(reducerFactory: ReducerFactory, block: suspend ReducerTestScope.() -> Unit) { +interface ReducerTest { + operator fun invoke(block: suspend ReducerTestScope.() -> Unit) +} + +fun testReducer(block: ((E) -> Unit) -> ReducerFactory): ReducerTest { + val fakeEventSource = FakeEventSource() + val reducerFactory = block(fakeEventSource) + return object : ReducerTest { + override fun invoke(block: suspend ReducerTestScope.() -> Unit) { + runReducerTest(reducerFactory, fakeEventSource, block) + } + } +} + +fun runReducerTest(reducerFactory: ReducerFactory, fakeEventSource: FakeEventSource, block: suspend ReducerTestScope.() -> Unit) { runTest { val expectTestScope = ExpectTest(coroutineContext) - block(ReducerTestScope(reducerFactory, expectTestScope)) + block(ReducerTestScope(reducerFactory, fakeEventSource, expectTestScope)) expectTestScope.verifyExpects() } } -class ReducerTestScope( +class ReducerTestScope( private val reducerFactory: ReducerFactory, + private val fakeEventSource: FakeEventSource, private val expectTestScope: ExpectTestScope ) : ExpectTestScope by expectTestScope, Reducer { @@ -51,7 +66,17 @@ class ReducerTestScope( reducerFactory.initialState() shouldBeEqualTo expected } - fun assertDispatches(expected: List) { + fun assertOnlyStateChange(expected: S) { + assertStateChange(expected) + assertNoDispatches() + fakeEventSource.assertNoEvents() + } + + fun assertStateChange(expected: S) { + capturedResult shouldBeEqualTo expected + } + + fun assertDispatches(expected: List) { assertEquals(expected, actionCaptures) } @@ -62,4 +87,22 @@ class ReducerTestScope( fun assertNoStateChange() { assertEquals(reducerFactory.initialState(), capturedResult) } + + fun assertOnlyDispatches(expected: List) { + assertDispatches(expected) + fakeEventSource.assertNoEvents() + assertNoStateChange() + } + + fun assertOnlyEvents(events: List) { + fakeEventSource.assertEvents(events) + assertNoDispatches() + assertNoStateChange() + } + + fun assertNoChanges() { + assertNoStateChange() + fakeEventSource.assertNoEvents() + assertNoDispatches() + } } \ No newline at end of file