Skip to content

Commit a740dfb

Browse files
committed
added wip tests
1 parent dcf786a commit a740dfb

File tree

6 files changed

+248
-0
lines changed

6 files changed

+248
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.nek12.flowMVI
2+
3+
import io.kotest.common.ExperimentalKotest
4+
import io.kotest.core.config.AbstractProjectConfig
5+
6+
class CoreTestConfig : AbstractProjectConfig() {
7+
@OptIn(ExperimentalKotest::class)
8+
override var testCoroutineDispatcher = true
9+
10+
override val coroutineDebugProbes: Boolean = true
11+
override val invocationTimeout = 5000L
12+
override val parallelism: Int = 1
13+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import TestState.Some
2+
import TestState.SomeData
3+
import app.cash.turbine.test
4+
import com.nek12.flowMVI.ActionShareBehavior.DISTRIBUTE
5+
import com.nek12.flowMVI.ActionShareBehavior.RESTRICT
6+
import com.nek12.flowMVI.ActionShareBehavior.SHARE
7+
import com.nek12.flowMVI.MVIStore
8+
import com.nek12.flowMVI.MVISubscriber
9+
import com.nek12.flowMVI.TestStore
10+
import com.nek12.flowMVI.subscribe
11+
import io.kotest.assertions.throwables.shouldThrowAny
12+
import io.kotest.assertions.throwables.shouldThrowExactly
13+
import io.kotest.core.spec.style.FreeSpec
14+
import io.kotest.core.test.testCoroutineScheduler
15+
import io.kotest.matchers.shouldBe
16+
import io.mockk.coVerify
17+
import io.mockk.mockk
18+
import kotlinx.coroutines.ExperimentalCoroutinesApi
19+
import kotlinx.coroutines.NonCancellable.cancel
20+
import kotlinx.coroutines.coroutineScope
21+
import kotlinx.coroutines.joinAll
22+
import kotlinx.coroutines.test.TestCoroutineScheduler
23+
import kotlinx.coroutines.test.TestScope
24+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
25+
import kotlinx.coroutines.test.advanceUntilIdle
26+
import kotlinx.coroutines.test.runCurrent
27+
import util.launched
28+
29+
30+
@OptIn(ExperimentalStdlibApi::class, ExperimentalCoroutinesApi::class)
31+
class StoreTest: FreeSpec({
32+
coroutineTestScope = true
33+
blockingTest = true
34+
concurrency = 1
35+
36+
"given store created" - {
37+
val state = Some
38+
val store = TestStore(state, RESTRICT) { SomeData("data") }
39+
"then state is ${state::class.simpleName}" {
40+
store.states.value shouldBe state
41+
}
42+
"then no actions" {
43+
store.actions.test {
44+
expectNoEvents()
45+
}
46+
}
47+
"then can be launched" - {
48+
var job = store.launch(this)
49+
50+
"and can't be launched twice" {
51+
shouldThrowExactly<IllegalArgumentException> {
52+
store.launch(this)
53+
}
54+
}
55+
"and can be canceled" {
56+
job.cancel()
57+
job.join()
58+
}
59+
"and can be launched again" {
60+
job = store.launch(this)
61+
job.cancel()
62+
job.join()
63+
}
64+
job.cancel()
65+
}
66+
}
67+
68+
"given store that sends actions and updates states" - {
69+
val state = SomeData("data")
70+
71+
val reduce: suspend MVIStore<TestState, TestIntent, TestAction>.(TestIntent) -> TestState = {
72+
send(TestAction.Some)
73+
state
74+
}
75+
"and 2 subscribers" - {
76+
77+
78+
"and action type is RESTRICT" - {
79+
"then throws" {
80+
//ensure scope is enclosed, otherwise exception will be thrown outside of assertion
81+
shouldThrowAny {
82+
coroutineScope {
83+
TestStore(Some, RESTRICT, reduce = reduce).launched(this@coroutineScope) {
84+
subscribe(this@coroutineScope, {}, {})
85+
subscribe(this@coroutineScope, {}, {})
86+
testCoroutineScheduler.advanceUntilIdle()
87+
}
88+
}
89+
}
90+
}
91+
}
92+
93+
"and action type is DISTRIBUTE" - {
94+
//todo: figure out what does kotest do wrong with the scope, that the subs don't work
95+
val scope = TestScope(testCoroutineScheduler)
96+
val sub1 = mockk<MVISubscriber<TestState, TestAction>>()
97+
val sub2 = mockk<MVISubscriber<TestState, TestAction>>()
98+
TestStore(Some, DISTRIBUTE, reduce = reduce).launched(scope) {
99+
sub1.subscribe(this, scope)
100+
sub2.subscribe(this, scope)
101+
"and intent received" - {
102+
send(TestIntent.Some)
103+
scope.advanceUntilIdle()
104+
"then one subscriber received action only" {
105+
coVerify(exactly = 1) { sub1.consume(TestAction.Some) }
106+
coVerify(exactly = 0) { sub2.consume(TestAction.Some) }
107+
108+
}
109+
// "then all subscribers updated state" {
110+
// coVerify(exactly = 1) { sub1.render(ofType<SomeData>()) }
111+
// coVerify(exactly = 1) { sub2.render(ofType<SomeData>()) }
112+
// }
113+
}
114+
}
115+
}
116+
117+
"and action type is SHARE" - {
118+
val scope = testScope
119+
val sub1 = mockk<MVISubscriber<TestState, TestAction>>()
120+
val sub2 = mockk<MVISubscriber<TestState, TestAction>>()
121+
TestStore(Some, SHARE, reduce = reduce).launched(scope) {
122+
sub1.subscribe(this@launched, scope)
123+
sub2.subscribe(this@launched, scope)
124+
"and intent received" - {
125+
send(TestIntent.Some)
126+
"then all subscribers received an action" {
127+
//todo: works, but because of scope does not arrive properly
128+
// coVerify(exactly = 1) { sub1.consume(TestAction.Some) }
129+
// coVerify(exactly = 1) { sub2.consume(TestAction.Some) }
130+
}
131+
}
132+
}
133+
}
134+
}
135+
}
136+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import com.nek12.flowMVI.MVIAction
2+
import com.nek12.flowMVI.MVIIntent
3+
import com.nek12.flowMVI.MVIState
4+
5+
sealed class TestState: MVIState {
6+
object Some: TestState()
7+
data class SomeData(val data: String): TestState()
8+
}
9+
10+
sealed class TestAction: MVIAction {
11+
object Some: TestAction()
12+
data class SomeData(val data: String): TestAction()
13+
}
14+
15+
sealed class TestIntent: MVIIntent {
16+
object Some: TestIntent()
17+
data class SomeData(val data: String): TestIntent()
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@file:Suppress("TestFunctionName")
2+
3+
package com.nek12.flowMVI
4+
5+
import TestAction
6+
import TestIntent
7+
import TestState
8+
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.flow.emptyFlow
10+
11+
internal fun TestStore(
12+
initialState: TestState,
13+
behavior: ActionShareBehavior,
14+
recover: MVIStore<TestState, TestIntent, TestAction>.(e: Exception) -> TestState = { throw it },
15+
reduce: suspend MVIStore<TestState, TestIntent, TestAction>.(TestIntent) -> TestState,
16+
) = MVIStore(initialState, behavior, recover, reduce)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package util
2+
3+
import com.nek12.flowMVI.MVIAction
4+
import com.nek12.flowMVI.MVIIntent
5+
import com.nek12.flowMVI.MVIState
6+
import com.nek12.flowMVI.MVIStore
7+
import io.kotest.core.test.testCoroutineScheduler
8+
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.ExperimentalCoroutinesApi
10+
11+
suspend inline fun <S: MVIState, I: MVIIntent, A: MVIAction>
12+
MVIStore<S, I, A>.launched(scope: CoroutineScope, block: MVIStore<S, I, A>.() -> Unit) = launch(scope).apply {
13+
block()
14+
cancel()
15+
join()
16+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package util
2+
3+
import com.nek12.flowMVI.MVIAction
4+
import com.nek12.flowMVI.MVIProvider
5+
import com.nek12.flowMVI.MVIState
6+
import com.nek12.flowMVI.MVISubscriber
7+
import com.nek12.flowMVI.subscribe
8+
import kotlinx.coroutines.CoroutineScope
9+
10+
class TestSubscriber<S: MVIState, A: MVIAction>(
11+
val render: (S) -> Unit = {},
12+
val consume: (A) -> Unit = {},
13+
): MVISubscriber<S, A> {
14+
15+
var counter = 0
16+
private set
17+
18+
private val _states = mutableListOf<S>()
19+
val states: List<S> get() = _states
20+
21+
private val _actions = mutableListOf<A>()
22+
val actions: List<A> get() = _actions
23+
24+
25+
override fun render(state: S) {
26+
++counter
27+
_states.add(state)
28+
render.invoke(state)
29+
}
30+
31+
override fun consume(action: A) {
32+
++counter
33+
_actions.add(action)
34+
consume.invoke(action)
35+
}
36+
37+
fun reset() {
38+
_states.clear()
39+
_actions.clear()
40+
}
41+
42+
suspend inline fun subscribed(provider: MVIProvider<S, *, A>, scope: CoroutineScope, test: TestSubscriber<S, A>.() -> Unit) =
43+
subscribe(provider, scope).apply {
44+
test()
45+
cancel()
46+
join()
47+
reset()
48+
}
49+
}

0 commit comments

Comments
 (0)