more tests around the directory reducer and helpers

This commit is contained in:
Adam Brown 2022-11-01 09:09:15 +00:00
parent 6c3006142b
commit 86f640d301
4 changed files with 89 additions and 30 deletions

View File

@ -27,6 +27,7 @@ inline fun <reified S, E> ComponentActivity.state(
noinline factory: () -> StateViewModel<S, E> noinline factory: () -> StateViewModel<S, E>
): Lazy<State<S, E>> { ): Lazy<State<S, E>> {
val factoryPromise = object : Factory { val factoryPromise = object : Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when(modelClass) { return when(modelClass) {
StateViewModel::class.java -> factory() as T StateViewModel::class.java -> factory() as T
@ -34,7 +35,7 @@ inline fun <reified S, E> ComponentActivity.state(
} }
} }
} }
return FooViewModelLazy( return KeyedViewModelLazy(
key = S::class.java.canonicalName!!, key = S::class.java.canonicalName!!,
StateViewModel::class, StateViewModel::class,
{ viewModelStore }, { viewModelStore },
@ -42,12 +43,11 @@ inline fun <reified S, E> ComponentActivity.state(
) as Lazy<State<S, E>> ) as Lazy<State<S, E>>
} }
class FooViewModelLazy<VM : ViewModel> @JvmOverloads constructor( class KeyedViewModelLazy<VM : ViewModel> @JvmOverloads constructor(
private val key: String, private val key: String,
private val viewModelClass: KClass<VM>, private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore, private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory, private val factoryProducer: () -> ViewModelProvider.Factory,
private val extrasProducer: () -> CreationExtras = { CreationExtras.Empty }
) : Lazy<VM> { ) : Lazy<VM> {
private var cached: VM? = null private var cached: VM? = null
@ -60,7 +60,7 @@ class FooViewModelLazy<VM : ViewModel> @JvmOverloads constructor(
ViewModelProvider( ViewModelProvider(
store, store,
factory, factory,
extrasProducer() CreationExtras.Empty
).get(key, viewModelClass.java).also { ).get(key, viewModelClass.java).also {
cached = it cached = it
} }

View File

@ -3,6 +3,7 @@ package app.dapk.st.directory
import app.dapk.st.directory.state.* import app.dapk.st.directory.state.*
import app.dapk.st.engine.DirectoryItem import app.dapk.st.engine.DirectoryItem
import app.dapk.st.engine.UnreadCount import app.dapk.st.engine.UnreadCount
import app.dapk.state.ReducerFactory
import fake.FakeChatEngine import fake.FakeChatEngine
import fixture.aRoomOverview import fixture.aRoomOverview
import io.mockk.mockk import io.mockk.mockk
@ -18,61 +19,71 @@ class DirectoryReducerTest {
private val fakeShortcutHandler = FakeShortcutHandler() private val fakeShortcutHandler = FakeShortcutHandler()
private val fakeChatEngine = FakeChatEngine() private val fakeChatEngine = FakeChatEngine()
private val fakeJobBag = FakeJobBag() private val fakeJobBag = FakeJobBag()
private val fakeEventSource = FakeEventSource<DirectoryEvent>()
private val reducer = directoryReducer( private val runReducerTest = testReducer { fakeEventSource ->
fakeChatEngine, directoryReducer(
fakeShortcutHandler.instance, fakeChatEngine,
fakeJobBag.instance, fakeShortcutHandler.instance,
fakeEventSource, fakeJobBag.instance,
) fakeEventSource,
)
}
@Test @Test
fun `initial state is empty loading`() = runReducerTest(reducer) { fun `initial state is empty loading`() = runReducerTest {
assertInitialState(DirectoryScreenState.EmptyLoading) assertInitialState(DirectoryScreenState.EmptyLoading)
} }
@Test @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)) } fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(listOf(AN_OVERVIEW)) }
fakeJobBag.instance.expect { it.add("sync", any()) } fakeJobBag.instance.expect { it.add("sync", any()) }
fakeChatEngine.givenDirectory().returns(flowOf(listOf(AN_OVERVIEW_STATE))) fakeChatEngine.givenDirectory().returns(flowOf(listOf(AN_OVERVIEW_STATE)))
reduce(ComponentLifecycle.OnVisible) reduce(ComponentLifecycle.OnVisible)
assertNoStateChange() assertOnlyDispatches(listOf(DirectoryStateChange.Content(listOf(AN_OVERVIEW_STATE))))
assertDispatches(listOf(DirectoryStateChange.Content(listOf(AN_OVERVIEW_STATE))))
} }
@Test @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()) } fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(emptyList()) }
fakeJobBag.instance.expect { it.add("sync", any()) } fakeJobBag.instance.expect { it.add("sync", any()) }
fakeChatEngine.givenDirectory().returns(flowOf(emptyList())) fakeChatEngine.givenDirectory().returns(flowOf(emptyList()))
reduce(ComponentLifecycle.OnVisible) reduce(ComponentLifecycle.OnVisible)
assertNoStateChange() assertOnlyDispatches(listOf(DirectoryStateChange.Empty))
assertDispatches(listOf(DirectoryStateChange.Empty))
} }
@Test @Test
fun `when Gone, then cancels sync job`() = runReducerTest(reducer) { fun `when Gone, then cancels sync job`() = runReducerTest {
fakeJobBag.instance.expect { it.cancel("sync") } fakeJobBag.instance.expect { it.cancel("sync") }
reduce(ComponentLifecycle.OnGone) reduce(ComponentLifecycle.OnGone)
assertNoStateChange() assertNoChanges()
assertNoDispatches()
} }
@Test @Test
fun `given ScrollToTop, then emits Scroll event`() = runReducerTest(reducer) { fun `when ScrollToTop, then emits Scroll event`() = runReducerTest {
reduce(DirectorySideEffect.ScrollToTop) reduce(DirectorySideEffect.ScrollToTop)
assertNoStateChange() assertOnlyEvents(listOf(DirectoryEvent.ScrollToTop))
assertNoDispatches() }
fakeEventSource.assertEvents(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 { class FakeJobBag {
val instance = mockk<JobBag>() val instance = mockk<JobBag>()
} }

View File

@ -4,7 +4,7 @@ import org.amshove.kluent.internal.assertEquals
class FakeEventSource<E> : (E) -> Unit { class FakeEventSource<E> : (E) -> Unit {
val captures = mutableListOf<E>() private val captures = mutableListOf<E>()
override fun invoke(event: E) { override fun invoke(event: E) {
captures.add(event) captures.add(event)
@ -13,4 +13,8 @@ class FakeEventSource<E> : (E) -> Unit {
fun assertEvents(expected: List<E>) { fun assertEvents(expected: List<E>) {
assertEquals(expected, captures) assertEquals(expected, captures)
} }
fun assertNoEvents() {
assertEquals(emptyList(), captures)
}
} }

View File

@ -12,16 +12,31 @@ import org.amshove.kluent.shouldBeEqualTo
import test.ExpectTest import test.ExpectTest
import test.ExpectTestScope import test.ExpectTestScope
fun <S> runReducerTest(reducerFactory: ReducerFactory<S>, block: suspend ReducerTestScope<S>.() -> Unit) { interface ReducerTest<S, E> {
operator fun invoke(block: suspend ReducerTestScope<S, E>.() -> Unit)
}
fun <S, E> testReducer(block: ((E) -> Unit) -> ReducerFactory<S>): ReducerTest<S, E> {
val fakeEventSource = FakeEventSource<E>()
val reducerFactory = block(fakeEventSource)
return object : ReducerTest<S, E> {
override fun invoke(block: suspend ReducerTestScope<S, E>.() -> Unit) {
runReducerTest(reducerFactory, fakeEventSource, block)
}
}
}
fun <S, E> runReducerTest(reducerFactory: ReducerFactory<S>, fakeEventSource: FakeEventSource<E>, block: suspend ReducerTestScope<S, E>.() -> Unit) {
runTest { runTest {
val expectTestScope = ExpectTest(coroutineContext) val expectTestScope = ExpectTest(coroutineContext)
block(ReducerTestScope(reducerFactory, expectTestScope)) block(ReducerTestScope(reducerFactory, fakeEventSource, expectTestScope))
expectTestScope.verifyExpects() expectTestScope.verifyExpects()
} }
} }
class ReducerTestScope<S>( class ReducerTestScope<S, E>(
private val reducerFactory: ReducerFactory<S>, private val reducerFactory: ReducerFactory<S>,
private val fakeEventSource: FakeEventSource<E>,
private val expectTestScope: ExpectTestScope private val expectTestScope: ExpectTestScope
) : ExpectTestScope by expectTestScope, Reducer<S> { ) : ExpectTestScope by expectTestScope, Reducer<S> {
@ -51,7 +66,17 @@ class ReducerTestScope<S>(
reducerFactory.initialState() shouldBeEqualTo expected reducerFactory.initialState() shouldBeEqualTo expected
} }
fun <S> assertDispatches(expected: List<S>) { fun assertOnlyStateChange(expected: S) {
assertStateChange(expected)
assertNoDispatches()
fakeEventSource.assertNoEvents()
}
fun assertStateChange(expected: S) {
capturedResult shouldBeEqualTo expected
}
fun assertDispatches(expected: List<Action>) {
assertEquals(expected, actionCaptures) assertEquals(expected, actionCaptures)
} }
@ -62,4 +87,22 @@ class ReducerTestScope<S>(
fun assertNoStateChange() { fun assertNoStateChange() {
assertEquals(reducerFactory.initialState(), capturedResult) assertEquals(reducerFactory.initialState(), capturedResult)
} }
fun assertOnlyDispatches(expected: List<Action>) {
assertDispatches(expected)
fakeEventSource.assertNoEvents()
assertNoStateChange()
}
fun assertOnlyEvents(events: List<E>) {
fakeEventSource.assertEvents(events)
assertNoDispatches()
assertNoStateChange()
}
fun assertNoChanges() {
assertNoStateChange()
fakeEventSource.assertNoEvents()
assertNoDispatches()
}
} }