Skip to content

Commit 3abad2d

Browse files
authored
Merge pull request #391 from Runnect/test/add-viewmodel-unit-test-infra
test: ViewModel unit test 인프라 추가 및 첫 테스트 작성
2 parents 7c6ec2e + 79a3e37 commit 3abad2d

4 files changed

Lines changed: 190 additions & 0 deletions

File tree

app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ dependencies {
188188

189189
// Test
190190
testImplementation libs.junit
191+
testImplementation libs.mockk
192+
testImplementation libs.turbine
193+
testImplementation libs.kotlinx.coroutines.test
191194
androidTestImplementation libs.androidx.junit
192195
androidTestImplementation libs.androidx.espresso.core
193196
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.runnect.runnect.presentation.event
2+
3+
import android.content.Context
4+
import com.runnect.runnect.application.PreferenceManager
5+
import io.mockk.every
6+
import io.mockk.mockk
7+
import io.mockk.mockkObject
8+
import io.mockk.unmockkObject
9+
import org.junit.After
10+
import org.junit.Assert.assertFalse
11+
import org.junit.Assert.assertTrue
12+
import org.junit.Before
13+
import org.junit.Test
14+
15+
class VisitorModeManagerTest {
16+
17+
private val context: Context = mockk()
18+
private lateinit var visitorModeManager: VisitorModeManager
19+
20+
@Before
21+
fun setUp() {
22+
mockkObject(PreferenceManager)
23+
visitorModeManager = VisitorModeManager(context)
24+
}
25+
26+
@After
27+
fun tearDown() {
28+
unmockkObject(PreferenceManager)
29+
}
30+
31+
@Test
32+
fun `access token이 visitor면 방문자 모드다`() {
33+
every { PreferenceManager.getString(any(), any()) } returns "visitor"
34+
35+
assertTrue(visitorModeManager.isVisitorMode)
36+
}
37+
38+
@Test
39+
fun `access token이 실제 로그인 토큰이면 방문자 모드가 아니다`() {
40+
every { PreferenceManager.getString(any(), any()) } returns "eyJhbGciOiJIUzI1NiJ9.sometoken"
41+
42+
assertFalse(visitorModeManager.isVisitorMode)
43+
}
44+
45+
@Test
46+
fun `토큰이 저장된 적 없는 기본값(none)이면 방문자 모드가 아니다`() {
47+
every { PreferenceManager.getString(any(), any()) } returns "none"
48+
49+
assertFalse(visitorModeManager.isVisitorMode)
50+
}
51+
52+
@Test
53+
fun `access token이 빈 문자열이면 EXPIRED로 처리되어 방문자 모드가 아니다`() {
54+
every { PreferenceManager.getString(any(), any()) } returns ""
55+
56+
assertFalse(visitorModeManager.isVisitorMode)
57+
}
58+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package com.runnect.runnect.presentation.mypage
2+
3+
import app.cash.turbine.test
4+
import app.cash.turbine.turbineScope
5+
import com.runnect.runnect.domain.entity.User
6+
import com.runnect.runnect.domain.repository.UserRepository
7+
import io.mockk.coEvery
8+
import io.mockk.mockk
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.ExperimentalCoroutinesApi
11+
import kotlinx.coroutines.delay
12+
import kotlinx.coroutines.flow.flow
13+
import kotlinx.coroutines.test.StandardTestDispatcher
14+
import kotlinx.coroutines.test.resetMain
15+
import kotlinx.coroutines.test.runTest
16+
import kotlinx.coroutines.test.setMain
17+
import org.junit.After
18+
import org.junit.Assert.assertEquals
19+
import org.junit.Assert.assertFalse
20+
import org.junit.Assert.assertNull
21+
import org.junit.Assert.assertTrue
22+
import org.junit.Before
23+
import org.junit.Test
24+
25+
@OptIn(ExperimentalCoroutinesApi::class)
26+
class MyPageViewModelTest {
27+
28+
private val testDispatcher = StandardTestDispatcher()
29+
30+
private lateinit var userRepository: UserRepository
31+
private lateinit var viewModel: MyPageViewModel
32+
33+
@Before
34+
fun setUp() {
35+
Dispatchers.setMain(testDispatcher)
36+
userRepository = mockk()
37+
}
38+
39+
@After
40+
fun tearDown() {
41+
Dispatchers.resetMain()
42+
}
43+
44+
@Test
45+
fun `LoadUserInfo 성공 시 유저 정보로 상태가 갱신된다`() = runTest(testDispatcher) {
46+
val user = User(
47+
email = "runner@runnect.com",
48+
latestStamp = "CSPR0",
49+
level = 3,
50+
levelPercent = 42,
51+
nickname = "러너"
52+
)
53+
coEvery { userRepository.getUserInfo() } returns flow {
54+
delay(1) // onLoading 상태를 별도 프레임으로 관찰하기 위한 실제 suspension 지점
55+
emit(Result.success(user))
56+
}
57+
viewModel = MyPageViewModel(userRepository)
58+
59+
viewModel.state.test {
60+
val initial = awaitItem()
61+
assertEquals(MyPageUiState(), initial)
62+
assertTrue("초기 상태는 isLoading=true가 기본값", initial.isLoading)
63+
64+
viewModel.intent(MyPageIntent.LoadUserInfo)
65+
66+
// MyPageUiState의 isLoading 기본값이 true라서, onLoading reduce는
67+
// 초기 상태와 동일해 StateFlow가 별도로 emit하지 않고 곧바로 결과 상태로 넘어간다.
68+
val success = awaitItem()
69+
assertFalse(success.isLoading)
70+
assertEquals("러너", success.nickname)
71+
assertEquals("CSPR0", success.stampId)
72+
assertEquals("3", success.level)
73+
assertEquals(42, success.levelPercent)
74+
assertEquals("runner@runnect.com", success.email)
75+
assertNull(success.error)
76+
77+
cancelAndIgnoreRemainingEvents()
78+
}
79+
}
80+
81+
@Test
82+
fun `LoadUserInfo 실패 시 에러 상태와 ShowError 이펙트가 발생한다`() = runTest(testDispatcher) {
83+
coEvery { userRepository.getUserInfo() } returns flow {
84+
delay(1)
85+
throw RuntimeException("네트워크 오류")
86+
}
87+
viewModel = MyPageViewModel(userRepository)
88+
89+
turbineScope {
90+
val stateTurbine = viewModel.state.testIn(backgroundScope)
91+
val effectTurbine = viewModel.effect.testIn(backgroundScope)
92+
93+
assertEquals(MyPageUiState(), stateTurbine.awaitItem())
94+
95+
viewModel.intent(MyPageIntent.LoadUserInfo)
96+
97+
// 초기 상태가 이미 isLoading=true라 onLoading reduce는 별도로 emit되지 않는다.
98+
val failure = stateTurbine.awaitItem()
99+
assertFalse(failure.isLoading)
100+
assertEquals("네트워크 오류 (unknown)", failure.error)
101+
102+
val effect = effectTurbine.awaitItem()
103+
assertTrue(effect is MyPageEffect.ShowError)
104+
105+
stateTurbine.cancelAndIgnoreRemainingEvents()
106+
effectTurbine.cancelAndIgnoreRemainingEvents()
107+
}
108+
}
109+
110+
@Test
111+
fun `UpdateNickname 인텐트는 닉네임 상태를 갱신한다`() = runTest(testDispatcher) {
112+
viewModel = MyPageViewModel(userRepository)
113+
114+
viewModel.state.test {
115+
assertEquals(MyPageUiState(), awaitItem())
116+
117+
viewModel.intent(MyPageIntent.UpdateNickname("새닉네임"))
118+
119+
assertEquals("새닉네임", awaitItem().nickname)
120+
121+
cancelAndIgnoreRemainingEvents()
122+
}
123+
}
124+
}

gradle/libs.versions.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ foojay = "0.10.0"
8686
junit = "4.13.2"
8787
androidx-junit = "1.3.0"
8888
espresso = "3.7.0"
89+
mockk = "1.13.13"
90+
turbine = "1.2.0"
8991

9092
[libraries]
9193
# AndroidX
@@ -180,6 +182,9 @@ tedpermission-normal = { group = "io.github.ParkSangGwon", name = "tedpermission
180182
junit = { group = "junit", name = "junit", version.ref = "junit" }
181183
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-junit" }
182184
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
185+
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
186+
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
187+
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
183188

184189
[bundles]
185190
firebase = [

0 commit comments

Comments
 (0)