Skip to content

Commit 9d6c610

Browse files
authoredAug 22, 2024··
Merge pull request #87 from futuredapp/feature/matsem/GH-80-uc-improvements
GH-80 UseCases: Fix CancellationException handling and add tests
2 parents bdcce8f + da2895b commit 9d6c610

File tree

19 files changed

+495
-54
lines changed

19 files changed

+495
-54
lines changed
 

‎gradle/libs.versions.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ koin-ksp-compiler = { group = "io.insert-koin", name = "koin-ksp-compiler", vers
7171

7272
# KotlinX
7373
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
74+
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
7475
kotlinx-immutableCollections = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinx-immutableCollections" }
7576
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
7677
kotlinx-dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-dateTime" }
@@ -86,8 +87,7 @@ decompose-compose-ext = { group = "com.arkivanov.decompose", name = "extensions-
8687
essenty = { group = "com.arkivanov.essenty", name = "lifecycle", version.ref = "essenty" }
8788

8889
# Testing
89-
kotlin-testCommon = { group = "org.jetbrains.kotlin", name = "kotlin-test-common", version.ref = "kotlin" }
90-
kotlin-testAnnotationsCommon = { group = "org.jetbrains.kotlin", name = "kotlin-test-annotations-common", version.ref = "kotlin" }
90+
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
9191
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" }
9292
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
9393
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }

‎shared/app/build.gradle.kts

+1-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ kotlin {
6969

7070
commonTest {
7171
dependencies {
72-
implementation(libs.kotlin.testCommon)
73-
implementation(libs.kotlin.testAnnotationsCommon)
72+
implementation(libs.kotlin.test)
7473
}
7574
}
7675

‎shared/feature/build.gradle.kts

+1-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ kotlin {
6060

6161
commonTest {
6262
dependencies {
63-
implementation(libs.kotlin.testCommon)
64-
implementation(libs.kotlin.testAnnotationsCommon)
63+
implementation(libs.kotlin.test)
6564
}
6665
}
6766
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package app.futured.kmptemplate.feature.domain
2+
3+
import app.futured.kmptemplate.util.domain.FlowUseCase
4+
import kotlinx.coroutines.currentCoroutineContext
5+
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.flow
8+
import kotlinx.coroutines.isActive
9+
import org.koin.core.annotation.Factory
10+
import kotlin.time.Duration
11+
12+
@Factory
13+
internal class CounterUseCase : FlowUseCase<CounterUseCaseArgs, Long>() {
14+
15+
override fun build(args: CounterUseCaseArgs): Flow<Long> = flow {
16+
var counter = 0L
17+
while (currentCoroutineContext().isActive) {
18+
emit(counter++)
19+
delay(args.interval)
20+
}
21+
}
22+
}
23+
24+
internal data class CounterUseCaseArgs(val interval: Duration)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package app.futured.kmptemplate.feature.domain
2+
3+
import app.futured.kmptemplate.util.domain.UseCase
4+
import kotlinx.coroutines.delay
5+
import org.koin.core.annotation.Factory
6+
import kotlin.time.Duration.Companion.seconds
7+
8+
@Factory
9+
internal class SyncDataUseCase : UseCase<Unit, Nothing>() {
10+
11+
override suspend fun build(args: Unit): Nothing {
12+
delay(10.seconds)
13+
error("Jokes on you, there's no data to be fetched in here.")
14+
}
15+
}

‎shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstViewModel.kt

+36-18
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,62 @@
11
package app.futured.kmptemplate.feature.ui.first
22

3+
import app.futured.kmptemplate.feature.domain.CounterUseCase
4+
import app.futured.kmptemplate.feature.domain.CounterUseCaseArgs
5+
import app.futured.kmptemplate.feature.domain.SyncDataUseCase
36
import app.futured.kmptemplate.feature.navigation.signedin.tab.b.TabBNavigator
47
import app.futured.kmptemplate.resources.MR
58
import app.futured.kmptemplate.util.arch.SharedViewModel
69
import app.futured.kmptemplate.util.ext.update
710
import co.touchlab.kermit.Logger
811
import dev.icerock.moko.resources.desc.ResourceFormatted
912
import dev.icerock.moko.resources.desc.StringDesc
10-
import kotlin.time.Duration.Companion.milliseconds
11-
import kotlinx.coroutines.delay
1213
import kotlinx.coroutines.flow.MutableStateFlow
13-
import kotlinx.coroutines.isActive
1414
import org.koin.core.annotation.Factory
15+
import kotlin.time.Duration.Companion.milliseconds
1516

1617
@Factory
1718
internal class FirstViewModel(
1819
private val tabBNavigator: TabBNavigator,
20+
private val syncDataUseCase: SyncDataUseCase,
21+
private val counterUseCase: CounterUseCase,
1922
) : SharedViewModel<FirstViewState, FirstUiEvent>(),
2023
FirstScreen.Actions {
2124

25+
private val logger: Logger = Logger.withTag("FirstViewModel")
26+
2227
override val viewState: MutableStateFlow<FirstViewState> = MutableStateFlow(FirstViewState())
2328

2429
init {
25-
launchWithHandler {
26-
var counter = 0
27-
while (isActive) {
28-
update(viewState) {
29-
copy(
30-
text = StringDesc.ResourceFormatted(stringRes = MR.strings.first_screen_text, 1, counter++),
31-
)
32-
}
33-
34-
if (counter == 10) {
35-
Logger.withTag("FirstViewmodel").d { "Counter reached 10" }
36-
sendUiEvent(FirstUiEvent.ShowToast("Counter reached 10 🎉"))
37-
}
38-
39-
delay(200.milliseconds)
30+
syncData()
31+
observeCounter()
32+
}
33+
34+
private fun syncData() = syncDataUseCase.execute {
35+
onError { error ->
36+
logger.e(error) { error.message.toString() }
37+
}
38+
}
39+
40+
private fun observeCounter() = counterUseCase.execute(CounterUseCaseArgs(interval = 200.milliseconds)) {
41+
onNext { count ->
42+
updateCount(count)
43+
44+
if (count == 10L) {
45+
logger.d { "Conter reached 10" }
46+
sendUiEvent(FirstUiEvent.ShowToast("Counter reached 10 🎉"))
4047
}
4148
}
49+
onError { error ->
50+
logger.e(error) { "Counter error" }
51+
}
52+
}
53+
54+
private fun updateCount(count: Long) {
55+
update(viewState) {
56+
copy(
57+
text = StringDesc.ResourceFormatted(stringRes = MR.strings.first_screen_text, 1, count),
58+
)
59+
}
4260
}
4361

4462
override fun onBack() = tabBNavigator.pop()

‎shared/network/graphql/build.gradle.kts

+1-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ kotlin {
4242

4343
commonTest {
4444
dependencies {
45-
implementation(libs.kotlin.testCommon)
46-
implementation(libs.kotlin.testAnnotationsCommon)
45+
implementation(libs.kotlin.test)
4746
}
4847
}
4948
}

‎shared/network/rest/build.gradle.kts

+1-2
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ kotlin {
5757

5858
commonTest {
5959
dependencies {
60-
implementation(libs.kotlin.testCommon)
61-
implementation(libs.kotlin.testAnnotationsCommon)
60+
implementation(libs.kotlin.test)
6261
}
6362
}
6463
}

‎shared/persistence/build.gradle.kts

+1-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ kotlin {
3535

3636
commonTest {
3737
dependencies {
38-
implementation(libs.kotlin.testCommon)
39-
implementation(libs.kotlin.testAnnotationsCommon)
38+
implementation(libs.kotlin.test)
4039
}
4140
}
4241
}

‎shared/platform/build.gradle.kts

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ kotlin {
2929

3030
commonTest {
3131
dependencies {
32-
implementation(libs.kotlin.testCommon)
33-
implementation(libs.kotlin.testAnnotationsCommon)
32+
implementation(libs.kotlin.test)
3433
}
3534
}
3635
}

‎shared/util/build.gradle.kts

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ kotlin {
3232

3333
commonTest {
3434
dependencies {
35-
implementation(libs.kotlin.testCommon)
36-
implementation(libs.kotlin.testAnnotationsCommon)
35+
implementation(libs.kotlin.test)
36+
implementation(libs.kotlinx.coroutines.test)
3737
}
3838
}
3939
}

‎shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/FlowUseCaseExecutionScope.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,12 @@ interface FlowUseCaseExecutionScope : CoroutineScopeOwner {
5555
error is CancellationException -> {
5656
// ignore this exception
5757
}
58+
5859
error != null -> {
59-
// UseCaseErrorHandler.globalOnErrorLogger(error) todo
60+
UseCaseErrorHandler.globalOnErrorLogger(error)
6061
flowUseCaseConfig.onError(error)
6162
}
63+
6264
else -> flowUseCaseConfig.onComplete()
6365
}
6466
}
@@ -101,10 +103,12 @@ interface FlowUseCaseExecutionScope : CoroutineScopeOwner {
101103
error is CancellationException -> {
102104
// ignore this exception
103105
}
106+
104107
error != null -> {
105-
// UseCaseErrorHandler.globalOnErrorLogger(error) todo
108+
UseCaseErrorHandler.globalOnErrorLogger(error)
106109
flowUseCaseConfig.onError(error)
107110
}
111+
108112
else -> flowUseCaseConfig.onComplete()
109113
}
110114
}

‎shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/SingleUseCaseExecutionScope.kt

+19-18
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ package app.futured.kmptemplate.util.domain.scope
33
import app.futured.kmptemplate.util.domain.UseCase
44
import app.futured.kmptemplate.util.domain.error.UseCaseErrorHandler
55
import kotlinx.coroutines.CancellationException
6-
import kotlinx.coroutines.CoroutineDispatcher
76
import kotlinx.coroutines.CoroutineStart
7+
import kotlinx.coroutines.Dispatchers
88
import kotlinx.coroutines.async
99
import kotlinx.coroutines.launch
10-
import kotlinx.coroutines.withContext
1110

1211
interface SingleUseCaseExecutionScope : CoroutineScopeOwner {
1312

@@ -47,13 +46,20 @@ interface SingleUseCaseExecutionScope : CoroutineScopeOwner {
4746
}
4847

4948
useCaseConfig.onStart()
50-
deferred = viewModelScope.async(start = CoroutineStart.LAZY) {
51-
buildOnBg(args, getWorkerDispatcher())
52-
}
49+
deferred = viewModelScope
50+
.async(context = getWorkerDispatcher(), start = CoroutineStart.LAZY) {
51+
build(args)
52+
}
5353
.also {
54-
viewModelScope.launch {
55-
kotlin.runCatching { it.await() }
56-
.fold(useCaseConfig.onSuccess, useCaseConfig.onError)
54+
viewModelScope.launch(Dispatchers.Main) {
55+
try {
56+
useCaseConfig.onSuccess(it.await())
57+
} catch (cancellation: CancellationException) {
58+
// do nothing - this is normal way of suspend function interruption
59+
} catch (error: Throwable) {
60+
UseCaseErrorHandler.globalOnErrorLogger(error)
61+
useCaseConfig.onError(error)
62+
}
5763
}
5864
}
5965
}
@@ -79,12 +85,12 @@ interface SingleUseCaseExecutionScope : CoroutineScopeOwner {
7985
if (cancelPrevious) {
8086
deferred?.cancel()
8187
}
88+
8289
return try {
83-
val newDeferred = viewModelScope.async(start = CoroutineStart.LAZY) {
84-
buildOnBg(args, getWorkerDispatcher())
85-
}.also {
86-
deferred = it
87-
}
90+
val newDeferred = viewModelScope.async(getWorkerDispatcher(), CoroutineStart.LAZY) {
91+
build(args)
92+
}.also { deferred = it }
93+
8894
Result.success(newDeferred.await())
8995
} catch (exception: CancellationException) {
9096
throw exception
@@ -93,11 +99,6 @@ interface SingleUseCaseExecutionScope : CoroutineScopeOwner {
9399
}
94100
}
95101

96-
private suspend fun <ARGS, T : Any?> UseCase<ARGS, T>.buildOnBg(
97-
args: ARGS,
98-
workerDispatcher: CoroutineDispatcher,
99-
) = withContext(workerDispatcher) { build(args) }
100-
101102
/**
102103
* Holds references to lambdas and some basic configuration
103104
* used to process results of Coroutine use case.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
package app.futured.kmptemplate.util.domain
2+
3+
import app.futured.kmptemplate.util.domain.base.BaseUseCaseExecutionScopeTest
4+
import app.futured.kmptemplate.util.domain.error.UseCaseErrorHandler
5+
import app.futured.kmptemplate.util.domain.usecases.TestFailureFlowUseCase
6+
import app.futured.kmptemplate.util.domain.usecases.TestFailureUseCase
7+
import app.futured.kmptemplate.util.domain.usecases.TestFlowUseCase
8+
import app.futured.kmptemplate.util.domain.usecases.TestUseCase
9+
import kotlinx.coroutines.ExperimentalCoroutinesApi
10+
import kotlinx.coroutines.launch
11+
import kotlinx.coroutines.test.TestScope
12+
import kotlin.coroutines.cancellation.CancellationException
13+
import kotlin.test.AfterTest
14+
import kotlin.test.BeforeTest
15+
import kotlin.test.Test
16+
import kotlin.test.assertEquals
17+
import kotlin.test.assertNotNull
18+
import kotlin.test.assertNull
19+
import kotlin.test.assertTrue
20+
import kotlin.test.fail
21+
22+
/**
23+
* Sanity check UseCase tests ported from [Arkitekt](https://github.com/futuredapp/arkitekt).
24+
*/
25+
class UseCaseExecutionScopeTest : BaseUseCaseExecutionScopeTest() {
26+
27+
@BeforeTest
28+
fun setUp() {
29+
UseCaseErrorHandler.globalOnErrorLogger = {}
30+
}
31+
32+
@AfterTest
33+
fun tearDown() {
34+
UseCaseErrorHandler.globalOnErrorLogger = {}
35+
}
36+
37+
@Test
38+
fun `given 1s delay use case when executed two times then first execution cancelled`() {
39+
val testUseCase = TestUseCase()
40+
var executionCount = 0
41+
42+
testUseCase.execute(1) {
43+
onSuccess { executionCount++ }
44+
onError {
45+
fail("Exception thrown where shouldn't")
46+
}
47+
}
48+
viewModelScope.advanceTimeByCompat(500)
49+
50+
testUseCase.execute(1) {
51+
onSuccess { executionCount++ }
52+
onError { fail("Exception thrown where shouldn't") }
53+
}
54+
viewModelScope.advanceTimeByCompat(1000)
55+
56+
assertEquals(1, executionCount)
57+
}
58+
59+
@Test
60+
fun `given failing test use case when executed then indicates onError`() {
61+
val testFailureUseCase = TestFailureUseCase()
62+
var resultError: Throwable? = null
63+
64+
testFailureUseCase.execute(IllegalStateException()) {
65+
onError { resultError = it }
66+
}
67+
viewModelScope.advanceTimeByCompat(1000)
68+
69+
assertNotNull(resultError)
70+
}
71+
72+
@Test
73+
fun `given test flow use case when executed two times then first execution cancelled`() {
74+
val testFlowUseCase = TestFlowUseCase()
75+
val testingList = listOfNotNull(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
76+
val resultList = mutableListOf<Int>()
77+
78+
testFlowUseCase.execute(TestFlowUseCase.Data(testingList, 1000)) {
79+
onNext { resultList.add(it) }
80+
onError { fail("Exception thrown where shouldn't") }
81+
onComplete { fail("onComplete called where shouldn't") }
82+
}
83+
84+
testFlowUseCase.execute(TestFlowUseCase.Data(testingList, 1000)) {
85+
onNext { resultList.add(it) }
86+
onError { fail("Exception thrown where shouldn't") }
87+
}
88+
viewModelScope.advanceTimeByCompat(10000)
89+
90+
assertEquals(testingList, resultList)
91+
}
92+
93+
@Test
94+
fun `given test flow use case when executed and all items emitted then completes`() {
95+
val testFlowUseCase = TestFlowUseCase()
96+
var completed = false
97+
val testingList = listOfNotNull(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
98+
99+
testFlowUseCase.execute(TestFlowUseCase.Data(testingList, 1000)) {
100+
onError { fail("Exception thrown where shouldn't") }
101+
onComplete { completed = true }
102+
}
103+
viewModelScope.advanceTimeByCompat(10000)
104+
105+
assertEquals(true, completed)
106+
}
107+
108+
@Test
109+
fun `given failing flow use case when executed then indicates onError`() {
110+
val testFlowFailureUseCase = TestFailureFlowUseCase()
111+
var resultError: Throwable? = null
112+
113+
testFlowFailureUseCase.execute(IllegalStateException()) {
114+
onNext { fail("onNext called where shouldn't") }
115+
onError { resultError = it }
116+
onComplete { fail("onComplete called where shouldn't") }
117+
}
118+
viewModelScope.advanceTimeByCompat(1000)
119+
120+
assertNotNull(resultError)
121+
}
122+
123+
@Test
124+
fun `given success use case launched in coroutine then result is set to success`() {
125+
val testUseCase = TestUseCase()
126+
127+
var result: Result<Int>? = null
128+
viewModelScope.launch {
129+
result = testUseCase.execute(1)
130+
}
131+
viewModelScope.advanceTimeByCompat(10000)
132+
133+
assertEquals(Result.success(1), result)
134+
}
135+
136+
@Test
137+
fun `given failing use case launched in coroutine then result is set to error`() {
138+
val testUseCase = TestFailureUseCase()
139+
140+
var result: Result<Unit>? = null
141+
viewModelScope.launch {
142+
result = testUseCase.execute(IllegalStateException())
143+
}
144+
viewModelScope.advanceTimeByCompat(10000)
145+
146+
assertTrue { result?.isFailure == true }
147+
assertTrue { result?.exceptionOrNull() is IllegalStateException }
148+
}
149+
150+
@Test
151+
fun `given failing use case with CancellationException launched in coroutine then error is rethrown`() {
152+
val testUseCase = TestFailureUseCase()
153+
154+
var result: Result<Unit>? = null
155+
viewModelScope.launch {
156+
result = testUseCase.execute(CancellationException())
157+
}
158+
viewModelScope.advanceTimeByCompat(10000)
159+
160+
assertNull(result)
161+
}
162+
163+
@Test
164+
fun `given success use case launched two times in coroutine then the first one is cancelled`() {
165+
val testUseCase = TestUseCase()
166+
167+
var result: Result<Int>? = null
168+
viewModelScope.launch {
169+
testUseCase.execute(1)
170+
fail("Execute should be cancelled")
171+
}
172+
viewModelScope.launch {
173+
result = testUseCase.execute(1)
174+
}
175+
viewModelScope.advanceTimeByCompat(10000)
176+
177+
assertEquals(Result.success(1), result)
178+
}
179+
180+
@Test
181+
fun `given success use case launched two times with cancelPrevious set to false in coroutine then the first one is not cancelled`() {
182+
val testUseCase = TestUseCase()
183+
184+
var result1: Result<Int>? = null
185+
var result2: Result<Int>? = null
186+
viewModelScope.launch {
187+
result1 = testUseCase.execute(1, cancelPrevious = false)
188+
}
189+
viewModelScope.launch {
190+
result2 = testUseCase.execute(2, cancelPrevious = false)
191+
}
192+
viewModelScope.advanceTimeByCompat(10000)
193+
194+
assertEquals(Result.success(1), result1)
195+
assertEquals(Result.success(2), result2)
196+
}
197+
198+
@Test
199+
fun `when launchWithHandler throws an exception then this exception is send to logUnhandledException and defaultErrorHandler`() {
200+
var logException: Throwable? = null
201+
var handlerException: Throwable? = null
202+
val testOwner = object : BaseUseCaseExecutionScopeTest() {
203+
override fun defaultErrorHandler(exception: Throwable) {
204+
handlerException = exception
205+
}
206+
}
207+
UseCaseErrorHandler.globalOnErrorLogger = { exception ->
208+
logException = exception
209+
}
210+
211+
val exception = IllegalStateException()
212+
testOwner.launchWithHandler { throw exception }
213+
testOwner.viewModelScope.advanceTimeByCompat(10000)
214+
215+
assertEquals(exception, logException)
216+
assertEquals(exception, handlerException)
217+
}
218+
219+
@Test
220+
fun `when launchWithHandler throws an CancellationException then this exception is not send to logUnhandledException and defaultErrorHandler`() {
221+
var logException: Throwable? = null
222+
var handlerException: Throwable? = null
223+
val testOwner = object : BaseUseCaseExecutionScopeTest() {
224+
override fun defaultErrorHandler(exception: Throwable) {
225+
handlerException = exception
226+
}
227+
}
228+
UseCaseErrorHandler.globalOnErrorLogger = { exception ->
229+
logException = exception
230+
}
231+
232+
val exception = CancellationException()
233+
testOwner.launchWithHandler { throw exception }
234+
testOwner.viewModelScope.advanceTimeByCompat(10000)
235+
236+
assertEquals(null, logException)
237+
assertEquals(null, handlerException)
238+
}
239+
240+
@Test
241+
fun `when launchWithHandler throws an CancellationException with non cancellation cause then this exception is send to logUnhandledException only`() {
242+
var logException: Throwable? = null
243+
var handlerException: Throwable? = null
244+
val testOwner = object : BaseUseCaseExecutionScopeTest() {
245+
override fun defaultErrorHandler(exception: Throwable) {
246+
handlerException = exception
247+
}
248+
}
249+
UseCaseErrorHandler.globalOnErrorLogger = { exception ->
250+
logException = exception
251+
}
252+
253+
val exception = CancellationException("Message", cause = IllegalStateException())
254+
testOwner.launchWithHandler { throw exception }
255+
testOwner.viewModelScope.advanceTimeByCompat(10000)
256+
257+
assertEquals(exception, logException)
258+
assertEquals(null, handlerException)
259+
}
260+
261+
@Test
262+
fun `when useCase is executed and onError is called then globalOnErrorLogger is called`() {
263+
val errorUseCase = TestFailureUseCase()
264+
var logException: Throwable? = null
265+
UseCaseErrorHandler.globalOnErrorLogger = { exception ->
266+
logException = exception
267+
}
268+
269+
var resultError: Throwable? = null
270+
errorUseCase.execute(IllegalStateException()) {
271+
onError { error ->
272+
resultError = error
273+
}
274+
}
275+
viewModelScope.advanceTimeByCompat(10000)
276+
277+
assertTrue(resultError is IllegalStateException)
278+
assertTrue(logException is IllegalStateException)
279+
}
280+
281+
@Test
282+
fun `when flowUseCase is executed and onError is called then globalOnErrorLogger is called`() {
283+
val errorUseCase = TestFailureFlowUseCase()
284+
var logException: Throwable? = null
285+
UseCaseErrorHandler.globalOnErrorLogger = { exception ->
286+
logException = exception
287+
}
288+
289+
var resultError: Throwable? = null
290+
errorUseCase.execute(IllegalStateException()) {
291+
onError { error ->
292+
resultError = error
293+
}
294+
}
295+
viewModelScope.advanceTimeByCompat(10000)
296+
297+
assertTrue(resultError is IllegalStateException)
298+
assertTrue(logException is IllegalStateException)
299+
}
300+
301+
@OptIn(ExperimentalCoroutinesApi::class)
302+
private fun TestScope.advanceTimeByCompat(delayTimeMillis: Long) {
303+
this.testScheduler.apply { advanceTimeBy(delayTimeMillis); runCurrent() }
304+
}
305+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package app.futured.kmptemplate.util.domain.base
2+
3+
import app.futured.kmptemplate.util.domain.scope.UseCaseExecutionScope
4+
import kotlinx.coroutines.CoroutineDispatcher
5+
import kotlinx.coroutines.Dispatchers
6+
import kotlinx.coroutines.test.StandardTestDispatcher
7+
import kotlinx.coroutines.test.TestScope
8+
import kotlinx.coroutines.test.resetMain
9+
import kotlinx.coroutines.test.setMain
10+
import kotlin.test.AfterTest
11+
import kotlin.test.BeforeTest
12+
13+
abstract class BaseUseCaseExecutionScopeTest : UseCaseExecutionScope {
14+
15+
private val testDispatcher = StandardTestDispatcher()
16+
override val viewModelScope = TestScope(testDispatcher)
17+
override fun getWorkerDispatcher(): CoroutineDispatcher = testDispatcher
18+
19+
@BeforeTest
20+
fun setDispatchers() {
21+
Dispatchers.setMain(testDispatcher)
22+
}
23+
24+
@AfterTest
25+
fun cleanupCoroutines() {
26+
Dispatchers.resetMain()
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package app.futured.kmptemplate.util.domain.usecases
2+
3+
import app.futured.kmptemplate.util.domain.FlowUseCase
4+
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.flow.flow
6+
7+
class TestFailureFlowUseCase : FlowUseCase<Throwable, Unit>() {
8+
9+
override fun build(args: Throwable): Flow<Unit> = flow {
10+
throw args
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package app.futured.kmptemplate.util.domain.usecases
2+
3+
import app.futured.kmptemplate.util.domain.UseCase
4+
5+
class TestFailureUseCase : UseCase<Throwable, Unit>() {
6+
7+
override suspend fun build(args: Throwable) {
8+
throw args
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package app.futured.kmptemplate.util.domain.usecases
2+
3+
import app.futured.kmptemplate.util.domain.FlowUseCase
4+
import kotlinx.coroutines.delay
5+
import kotlinx.coroutines.flow.Flow
6+
import kotlinx.coroutines.flow.asFlow
7+
import kotlinx.coroutines.flow.onEach
8+
9+
class TestFlowUseCase : FlowUseCase<TestFlowUseCase.Data, Int>() {
10+
11+
data class Data(
12+
val listToEmit: List<Int>,
13+
val delayBetweenEmits: Long
14+
)
15+
16+
override fun build(args: Data): Flow<Int> =
17+
args.listToEmit.asFlow().onEach { delay(args.delayBetweenEmits) }
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package app.futured.kmptemplate.util.domain.usecases
2+
3+
import app.futured.kmptemplate.util.domain.UseCase
4+
import kotlinx.coroutines.delay
5+
6+
class TestUseCase : UseCase<Int, Int>() {
7+
8+
override suspend fun build(args: Int): Int {
9+
delay(1000)
10+
return args
11+
}
12+
}

0 commit comments

Comments
 (0)
Please sign in to comment.