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

View File

@ -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<DirectoryEvent>()
private val reducer = directoryReducer(
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<JobBag>()
}

View File

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

View File

@ -12,16 +12,31 @@ import org.amshove.kluent.shouldBeEqualTo
import test.ExpectTest
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 {
val expectTestScope = ExpectTest(coroutineContext)
block(ReducerTestScope(reducerFactory, expectTestScope))
block(ReducerTestScope(reducerFactory, fakeEventSource, expectTestScope))
expectTestScope.verifyExpects()
}
}
class ReducerTestScope<S>(
class ReducerTestScope<S, E>(
private val reducerFactory: ReducerFactory<S>,
private val fakeEventSource: FakeEventSource<E>,
private val expectTestScope: ExpectTestScope
) : ExpectTestScope by expectTestScope, Reducer<S> {
@ -51,7 +66,17 @@ class ReducerTestScope<S>(
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)
}
@ -62,4 +87,22 @@ class ReducerTestScope<S>(
fun assertNoStateChange() {
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()
}
}