Skip to content

Commit bef6879

Browse files
committed
Feat: MviViewModel 수정 및 테스트코드 작성
1 parent adf5f5d commit bef6879

4 files changed

Lines changed: 111 additions & 15 deletions

File tree

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ javax-inject = { group = "javax.inject", name = "javax.inject", version.ref = "j
9595
orbit-core = { group = "org.orbit-mvi", name = "orbit-core", version.ref = "orbit" }
9696
orbit-compose = { group = "org.orbit-mvi", name = "orbit-compose", version.ref = "orbit" }
9797
orbit-viewmodel = { group = "org.orbit-mvi", name = "orbit-viewmodel", version.ref = "orbit" }
98+
orbit-test = { group = "org.orbit-mvi", name = "orbit-test", version.ref = "orbit" }
9899

99100
## retrofit
100101
retrofit-bom = { group = "com.squareup.retrofit2", name = "retrofit-bom", version.ref = "retrofit" }
@@ -110,6 +111,7 @@ okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-i
110111
junit = { group = "junit", name = "junit", version.ref = "junit" }
111112
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
112113
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
114+
kotlin-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
113115

114116
## Other
115117
material = { group = "com.google.android.material", name = "material", version.ref = "material" }

presentation/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@ dependencies {
1313

1414
implementation(libs.bundles.androidx.core)
1515
implementation(libs.bundles.orbit)
16+
17+
testImplementation(libs.junit)
18+
testImplementation(libs.kotlin.coroutines.test)
19+
testImplementation(libs.orbit.test)
1620
}

presentation/src/main/java/com/threegap/bitnagil/presentation/common/mviviewmodel/MviViewModel.kt

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,32 @@ import androidx.lifecycle.SavedStateHandle
44
import androidx.lifecycle.ViewModel
55
import kotlinx.coroutines.flow.Flow
66
import kotlinx.coroutines.flow.StateFlow
7-
import org.orbitmvi.orbit.Container
87
import org.orbitmvi.orbit.ContainerHost
8+
import org.orbitmvi.orbit.syntax.simple.SimpleSyntax
99
import org.orbitmvi.orbit.syntax.simple.intent
1010
import org.orbitmvi.orbit.syntax.simple.postSideEffect
1111
import org.orbitmvi.orbit.syntax.simple.reduce
1212
import org.orbitmvi.orbit.viewmodel.container
1313

1414
abstract class MviViewModel<STATE : MviState, SIDE_EFFECT : MviSideEffect, INTENT : MviIntent>(
15-
private val initState: STATE,
16-
private val savedStateHandle: SavedStateHandle,
15+
initState: STATE,
16+
savedStateHandle: SavedStateHandle,
1717
) : ContainerHost<STATE, SIDE_EFFECT>, ViewModel() {
18-
override val container: Container<STATE, SIDE_EFFECT>
19-
get() = container(initialState = initState, savedStateHandle = savedStateHandle)
18+
override val container = container<STATE, SIDE_EFFECT>(initialState = initState, savedStateHandle = savedStateHandle)
2019

21-
val stateFlow: StateFlow<STATE> = container.stateFlow
20+
val stateFlow: StateFlow<STATE> get() = container.stateFlow
21+
val sideEffectFlow: Flow<SIDE_EFFECT> get() = container.sideEffectFlow
2222

23-
val sideEffectFlow: Flow<SIDE_EFFECT> = container.sideEffectFlow
23+
suspend fun SimpleSyntax<STATE, SIDE_EFFECT>.sendSideEffect(sideEffect: SIDE_EFFECT) = postSideEffect(sideEffect)
2424

25-
protected fun sendSideEffect(sideEffect: SIDE_EFFECT) =
26-
intent {
27-
postSideEffect(sideEffect)
28-
}
29-
30-
protected abstract fun reduceState(intent: INTENT): STATE?
25+
protected abstract suspend fun SimpleSyntax<STATE, SIDE_EFFECT>.reduceState(
26+
intent: INTENT,
27+
state: STATE,
28+
): STATE?
3129

32-
protected fun sendIntent(intent: INTENT) =
30+
fun sendIntent(intent: INTENT) =
3331
intent {
34-
val newState = reduceState(intent)
32+
val newState = reduceState(intent, state)
3533

3634
newState?.let {
3735
reduce { newState }
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.threegap.bitnagil.presentation.common.mviviewmodel
2+
3+
import androidx.lifecycle.SavedStateHandle
4+
import kotlinx.coroutines.ExperimentalCoroutinesApi
5+
import kotlinx.coroutines.test.runTest
6+
import kotlinx.parcelize.Parcelize
7+
import org.junit.Before
8+
import org.junit.Test
9+
import org.orbitmvi.orbit.syntax.simple.SimpleSyntax
10+
import org.orbitmvi.orbit.test.test
11+
12+
@ExperimentalCoroutinesApi
13+
class MviViewModelTest {
14+
private lateinit var sampleMviViewModel: SampleMviViewModel
15+
16+
@Before
17+
fun setUp() {
18+
sampleMviViewModel = SampleMviViewModel(initState = SampleState(), savedStateHandle = SavedStateHandle())
19+
}
20+
21+
@Test
22+
fun `state는 호출 순서대로 갱신되어야 한다`() =
23+
runTest {
24+
sampleMviViewModel.test(testScope = this) {
25+
containerHost.sendIntent(SampleIntent.Increase(number = 1))
26+
containerHost.sendIntent(SampleIntent.Decrease(number = 2))
27+
containerHost.sendIntent(SampleIntent.Increase(number = 3))
28+
29+
expectState { SampleState() }
30+
expectState { SampleState(count = 1) }
31+
expectState { SampleState(count = -1) }
32+
expectState { SampleState(count = 2) }
33+
}
34+
}
35+
36+
@Test
37+
fun `state와 sideEffect는 호출 순서대로 갱신되어야 한다`() =
38+
runTest {
39+
sampleMviViewModel.test(testScope = this) {
40+
containerHost.sendIntent(SampleIntent.Increase(number = 1))
41+
containerHost.sendIntent(SampleIntent.Clear)
42+
containerHost.sendIntent(SampleIntent.Decrease(number = 2))
43+
containerHost.sendIntent(SampleIntent.Increase(number = 3))
44+
45+
expectState { SampleState() }
46+
expectState { SampleState(count = 1) }
47+
expectSideEffect(SampleSideEffect.ShowToast("Clear"))
48+
expectState { SampleState() }
49+
expectState { SampleState(count = -2) }
50+
expectState { SampleState(count = 1) }
51+
}
52+
}
53+
54+
// only for test
55+
private class SampleMviViewModel(
56+
initState: SampleState,
57+
savedStateHandle: SavedStateHandle,
58+
) : MviViewModel<SampleState, SampleSideEffect, SampleIntent>(initState, savedStateHandle) {
59+
override suspend fun SimpleSyntax<SampleState, SampleSideEffect>.reduceState(
60+
intent: SampleIntent,
61+
state: SampleState,
62+
): SampleState? {
63+
val newState =
64+
when (intent) {
65+
is SampleIntent.Decrease -> state.copy(count = state.count - intent.number)
66+
is SampleIntent.Increase -> state.copy(count = state.count + intent.number)
67+
SampleIntent.Clear -> {
68+
sendSideEffect(sideEffect = SampleSideEffect.ShowToast("Clear"))
69+
state.copy(count = 0)
70+
}
71+
}
72+
return newState
73+
}
74+
}
75+
76+
@Parcelize
77+
private data class SampleState(
78+
val count: Int = 0,
79+
) : MviState
80+
81+
private sealed class SampleSideEffect : MviSideEffect {
82+
data class ShowToast(val message: String) : SampleSideEffect()
83+
}
84+
85+
private sealed class SampleIntent : MviIntent {
86+
data class Increase(val number: Int) : SampleIntent()
87+
88+
data class Decrease(val number: Int) : SampleIntent()
89+
90+
object Clear : SampleIntent()
91+
}
92+
}

0 commit comments

Comments
 (0)