Skip to content

Commit 661ec81

Browse files
committed
add SearchVMTest.kt
1 parent f2917ca commit 661ec81

File tree

1 file changed

+227
-23
lines changed

1 file changed

+227
-23
lines changed
Lines changed: 227 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package com.hoc.flowmvi.ui.search
22

33
import androidx.lifecycle.SavedStateHandle
4+
import arrow.core.left
45
import arrow.core.right
56
import com.flowmvi.mvi_testing.BaseMviViewModelTest
67
import com.flowmvi.mvi_testing.mapRight
8+
import com.hoc.flowmvi.domain.model.UserError
79
import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase
10+
import com.hoc.flowmvi.ui.search.SearchVM.Companion.SEARCH_DEBOUNCE_DURATION
11+
import com.hoc081098.flowext.concatWith
12+
import com.hoc081098.flowext.timer
813
import io.mockk.coEvery
914
import io.mockk.coVerify
1015
import io.mockk.confirmVerified
@@ -13,6 +18,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
1318
import kotlinx.coroutines.FlowPreview
1419
import kotlinx.coroutines.delay
1520
import kotlinx.coroutines.flow.flow
21+
import kotlinx.coroutines.flow.flowOf
22+
import kotlinx.coroutines.flow.map
23+
import kotlinx.coroutines.flow.onCompletion
24+
import kotlinx.coroutines.flow.onEach
1625
import kotlin.test.Test
1726
import kotlin.time.Duration
1827
import kotlin.time.ExperimentalTime
@@ -28,10 +37,7 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
2837
override fun setup() {
2938
super.setup()
3039

31-
searchUsersUseCase = mockk() {
32-
coEvery { this@mockk(any()) } returns USERS.right()
33-
}
34-
40+
searchUsersUseCase = mockk()
3541
savedStateHandle = SavedStateHandle()
3642
vm = SearchVM(
3743
searchUsersUseCase = searchUsersUseCase,
@@ -47,36 +53,188 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
4753
}
4854

4955
@Test
50-
fun test_withSearchIntent_rejectBlankSearchQuery() {
51-
val q = " "
56+
fun test_withSearchIntent_debounceSearchQuery() {
57+
val query = "d"
58+
coEvery { searchUsersUseCase(query) } returns USERS.right()
59+
5260
test(
5361
vmProducer = { vm },
54-
intents = flow {
55-
emit(ViewIntent.Search(q))
56-
timeout()
57-
},
62+
intents = flowOf("a", "b", "c", query)
63+
.map { ViewIntent.Search(it) }
64+
.onEach { delay(SEMI_TIMEOUT) }
65+
.onCompletion { timeout() },
5866
expectedStates = listOf(
5967
ViewState.initial(null),
6068
ViewState(
6169
users = emptyList(),
6270
isLoading = false,
6371
error = null,
6472
submittedQuery = "",
65-
originalQuery = q, // update originalQuery
73+
originalQuery = "a", // update originalQuery
74+
),
75+
ViewState(
76+
users = emptyList(),
77+
isLoading = false,
78+
error = null,
79+
submittedQuery = "",
80+
originalQuery = "b", // update originalQuery
81+
),
82+
ViewState(
83+
users = emptyList(),
84+
isLoading = false,
85+
error = null,
86+
submittedQuery = "",
87+
originalQuery = "c", // update originalQuery
88+
),
89+
ViewState(
90+
users = emptyList(),
91+
isLoading = false,
92+
error = null,
93+
submittedQuery = "",
94+
originalQuery = query, // update originalQuery
95+
),
96+
ViewState(
97+
users = emptyList(),
98+
isLoading = true, // update isLoading
99+
error = null,
100+
submittedQuery = "",
101+
originalQuery = query,
102+
),
103+
ViewState(
104+
users = USER_ITEMS, // update users
105+
isLoading = false, // update isLoading
106+
error = null,
107+
submittedQuery = query, // update submittedQuery
108+
originalQuery = query,
66109
),
67110
).mapRight(),
68111
expectedEvents = emptyList(),
69-
delayAfterDispatchingIntents = TIMEOUT,
112+
delayAfterDispatchingIntents = EXTRAS_TIMEOUT,
113+
) {
114+
coVerify(exactly = 1) { searchUsersUseCase(query) }
115+
}
116+
}
117+
118+
@Test
119+
fun test_withSearchIntent_debounceSearchQueryAndRejectBlank() {
120+
val query = " "
121+
122+
test(
123+
vmProducer = { vm },
124+
intents = flowOf("a", "b", "c", query)
125+
.map { ViewIntent.Search(it) }
126+
.onEach { delay(SEMI_TIMEOUT) }
127+
.onCompletion { timeout() },
128+
expectedStates = listOf(
129+
ViewState.initial(null),
130+
ViewState(
131+
users = emptyList(),
132+
isLoading = false,
133+
error = null,
134+
submittedQuery = "",
135+
originalQuery = "a", // update originalQuery
136+
),
137+
ViewState(
138+
users = emptyList(),
139+
isLoading = false,
140+
error = null,
141+
submittedQuery = "",
142+
originalQuery = "b", // update originalQuery
143+
),
144+
ViewState(
145+
users = emptyList(),
146+
isLoading = false,
147+
error = null,
148+
submittedQuery = "",
149+
originalQuery = "c", // update originalQuery
150+
),
151+
ViewState(
152+
users = emptyList(),
153+
isLoading = false,
154+
error = null,
155+
submittedQuery = "",
156+
originalQuery = query, // update originalQuery
157+
),
158+
).mapRight(),
159+
expectedEvents = emptyList(),
160+
delayAfterDispatchingIntents = EXTRAS_TIMEOUT,
70161
)
71162
}
72163

164+
@Test
165+
fun test_withSearchIntent_debounceSearchQueryThenRejectBlankAndDistinctUntilChanged() {
166+
val query = "#query"
167+
coEvery { searchUsersUseCase(query) } returns USERS.right()
168+
169+
test(
170+
vmProducer = { vm },
171+
intents = flowOf("a", "b", "c", query)
172+
.map { ViewIntent.Search(it) }
173+
.onEach { delay(SEMI_TIMEOUT) }
174+
.onCompletion { timeout() }
175+
.concatWith(timer(ViewIntent.Search(query), TOTAL_TIMEOUT))
176+
.onCompletion { timeout() },
177+
expectedStates = listOf(
178+
ViewState.initial(null),
179+
ViewState(
180+
users = emptyList(),
181+
isLoading = false,
182+
error = null,
183+
submittedQuery = "",
184+
originalQuery = "a", // update originalQuery
185+
),
186+
ViewState(
187+
users = emptyList(),
188+
isLoading = false,
189+
error = null,
190+
submittedQuery = "",
191+
originalQuery = "b", // update originalQuery
192+
),
193+
ViewState(
194+
users = emptyList(),
195+
isLoading = false,
196+
error = null,
197+
submittedQuery = "",
198+
originalQuery = "c", // update originalQuery
199+
),
200+
ViewState(
201+
users = emptyList(),
202+
isLoading = false,
203+
error = null,
204+
submittedQuery = "",
205+
originalQuery = query, // update originalQuery
206+
),
207+
ViewState(
208+
users = emptyList(),
209+
isLoading = true, // update isLoading
210+
error = null,
211+
submittedQuery = "",
212+
originalQuery = query,
213+
),
214+
ViewState(
215+
users = USER_ITEMS, // update users
216+
isLoading = false, // update isLoading
217+
error = null,
218+
submittedQuery = query, // update submittedQuery
219+
originalQuery = query,
220+
),
221+
).mapRight(),
222+
expectedEvents = emptyList(),
223+
delayAfterDispatchingIntents = EXTRAS_TIMEOUT,
224+
) {
225+
coVerify(exactly = 1) { searchUsersUseCase(query) }
226+
}
227+
}
228+
73229
@Test
74230
fun test_withSearchIntent_returnsUserItemsWithProperLoadingState() {
75-
val q = "query"
231+
val query = "query"
232+
coEvery { searchUsersUseCase(query) } returns USERS.right()
233+
76234
test(
77235
vmProducer = { vm },
78236
intents = flow {
79-
emit(ViewIntent.Search(q))
237+
emit(ViewIntent.Search(query))
80238
timeout()
81239
},
82240
expectedStates = listOf(
@@ -86,37 +244,83 @@ class SearchVMTest : BaseMviViewModelTest<ViewIntent, ViewState, SingleEvent, Se
86244
isLoading = false,
87245
error = null,
88246
submittedQuery = "",
89-
originalQuery = q, // update originalQuery
247+
originalQuery = query, // update originalQuery
90248
),
91249
ViewState(
92250
users = emptyList(),
93251
isLoading = true, // update isLoading
94252
error = null,
95253
submittedQuery = "",
96-
originalQuery = q,
254+
originalQuery = query,
97255
),
98256
ViewState(
99257
users = USER_ITEMS, // update users
100258
isLoading = false, // update isLoading
101259
error = null,
102-
submittedQuery = q,
103-
originalQuery = q,
260+
submittedQuery = query, // update submittedQuery
261+
originalQuery = query,
104262
),
105263
).mapRight(),
106264
expectedEvents = emptyList(),
107-
delayAfterDispatchingIntents = TIMEOUT,
265+
delayAfterDispatchingIntents = EXTRAS_TIMEOUT,
266+
) {
267+
coVerify(exactly = 1) { searchUsersUseCase(query) }
268+
}
269+
}
270+
271+
@Test
272+
fun test_withSearchIntent_returnsUserErrorWithProperLoadingState() {
273+
val query = "query"
274+
val networkError = UserError.NetworkError
275+
coEvery { searchUsersUseCase(query) } returns networkError.left()
276+
277+
test(
278+
vmProducer = { vm },
279+
intents = flow {
280+
emit(ViewIntent.Search(query))
281+
timeout()
282+
},
283+
expectedStates = listOf(
284+
ViewState.initial(null),
285+
ViewState(
286+
users = emptyList(),
287+
isLoading = false,
288+
error = null,
289+
submittedQuery = "",
290+
originalQuery = query, // update originalQuery
291+
),
292+
ViewState(
293+
users = emptyList(),
294+
isLoading = true, // update isLoading
295+
error = null,
296+
submittedQuery = "",
297+
originalQuery = query,
298+
),
299+
ViewState(
300+
users = emptyList(),
301+
isLoading = false, // update isLoading
302+
error = networkError, // update error
303+
submittedQuery = query, // update submittedQuery
304+
originalQuery = query,
305+
),
306+
).mapRight(),
307+
expectedEvents = listOf(
308+
SingleEvent.SearchFailure(networkError)
309+
).mapRight(),
310+
delayAfterDispatchingIntents = EXTRAS_TIMEOUT,
108311
) {
109-
coVerify(exactly = 1) { searchUsersUseCase(q) }
312+
coVerify(exactly = 1) { searchUsersUseCase(query) }
110313
}
111314
}
112315

113316
private companion object {
114-
private val TIMEOUT = Duration.milliseconds(100)
317+
private val EXTRAS_TIMEOUT = Duration.milliseconds(100)
318+
private val TOTAL_TIMEOUT = SEARCH_DEBOUNCE_DURATION + EXTRAS_TIMEOUT
319+
private val SEMI_TIMEOUT = SEARCH_DEBOUNCE_DURATION / 10
115320

116321
/**
117322
* Extra delay to emit search intent
118323
*/
119-
private suspend inline fun timeout() =
120-
delay(SearchVM.SEARCH_DEBOUNCE_DURATION + TIMEOUT)
324+
private suspend inline fun timeout() = delay(TOTAL_TIMEOUT)
121325
}
122326
}

0 commit comments

Comments
 (0)