Skip to content

Commit 0c16e65

Browse files
committed
chore: make ViewModels testable; add error validation tests for all vm's
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 88c1bfd commit 0c16e65

63 files changed

Lines changed: 1519 additions & 71 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.flipcash.app.inject
2+
3+
import com.flipcash.libs.coroutines.DispatcherProvider
4+
import com.flipcash.app.internal.dispatchers.DefaultDispatcherProvider
5+
import dagger.Module
6+
import dagger.Provides
7+
import dagger.hilt.InstallIn
8+
import dagger.hilt.components.SingletonComponent
9+
import javax.inject.Singleton
10+
11+
@Module
12+
@InstallIn(SingletonComponent::class)
13+
object DispatcherModule {
14+
15+
@Provides
16+
@Singleton
17+
fun providesDispatcherProvider(): DispatcherProvider = DefaultDispatcherProvider()
18+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.flipcash.app.internal.dispatchers
2+
3+
import com.flipcash.libs.coroutines.DispatcherProvider
4+
import kotlinx.coroutines.CoroutineDispatcher
5+
import kotlinx.coroutines.Dispatchers
6+
7+
class DefaultDispatcherProvider : DispatcherProvider {
8+
override val Default: CoroutineDispatcher = Dispatchers.Default
9+
override val Main: CoroutineDispatcher = Dispatchers.Main
10+
override val IO: CoroutineDispatcher = Dispatchers.IO
11+
}

apps/flipcash/core/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ android {
77
}
88

99
dependencies {
10+
testImplementation(kotlin("test"))
11+
testImplementation(libs.bundles.unit.testing)
12+
1013
implementation(libs.androidx.browser)
1114

1215
implementation(libs.kotlinx.serialization.core)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.flipcash.app.core
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlinx.coroutines.ExperimentalCoroutinesApi
5+
import kotlinx.coroutines.test.StandardTestDispatcher
6+
import kotlinx.coroutines.test.TestDispatcher
7+
import kotlinx.coroutines.test.resetMain
8+
import kotlinx.coroutines.test.setMain
9+
import org.junit.rules.TestWatcher
10+
import org.junit.runner.Description
11+
12+
@OptIn(ExperimentalCoroutinesApi::class)
13+
class MainCoroutineRule(
14+
val dispatcher: TestDispatcher = StandardTestDispatcher()
15+
) : TestWatcher() {
16+
17+
override fun starting(description: Description) {
18+
Dispatchers.setMain(dispatcher)
19+
}
20+
21+
override fun finished(description: Description) {
22+
Dispatchers.resetMain()
23+
}
24+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.flipcash.app.core.dispatchers
2+
3+
import com.flipcash.libs.coroutines.DispatcherProvider
4+
import kotlinx.coroutines.CoroutineDispatcher
5+
import kotlinx.coroutines.test.StandardTestDispatcher
6+
import kotlinx.coroutines.test.TestDispatcher
7+
8+
class TestDispatcherProvider(
9+
val testDispatcher: TestDispatcher = StandardTestDispatcher()
10+
) : DispatcherProvider {
11+
override val Default: CoroutineDispatcher get() = testDispatcher
12+
override val Main: CoroutineDispatcher get() = testDispatcher
13+
override val IO: CoroutineDispatcher get() = testDispatcher
14+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.flipcash.app.core.time
2+
3+
import org.junit.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.time.Duration.Companion.milliseconds
6+
import kotlin.time.Duration.Companion.minutes
7+
import kotlin.time.Duration.Companion.seconds
8+
import kotlin.time.Instant
9+
10+
class FakeTimeProviderTest {
11+
12+
@Test
13+
fun `now returns start time initially`() {
14+
val startTime = Instant.fromEpochMilliseconds(1_000_000L)
15+
val provider = FakeTimeProvider(startTime)
16+
17+
assertEquals(startTime, provider.now())
18+
}
19+
20+
@Test
21+
fun `currentTimeMillis returns epoch millis of now`() {
22+
val startTime = Instant.fromEpochMilliseconds(1_000_000L)
23+
val provider = FakeTimeProvider(startTime)
24+
25+
assertEquals(1_000_000L, provider.currentTimeMillis())
26+
}
27+
28+
@Test
29+
fun `advanceBy moves time forward`() {
30+
val startTime = Instant.fromEpochMilliseconds(0L)
31+
val provider = FakeTimeProvider(startTime)
32+
33+
provider.advanceBy(5.seconds)
34+
35+
assertEquals(5_000L, provider.currentTimeMillis())
36+
}
37+
38+
@Test
39+
fun `advanceBy accumulates across multiple calls`() {
40+
val startTime = Instant.fromEpochMilliseconds(0L)
41+
val provider = FakeTimeProvider(startTime)
42+
43+
provider.advanceBy(1.minutes)
44+
provider.advanceBy(30.seconds)
45+
46+
assertEquals(90_000L, provider.currentTimeMillis())
47+
}
48+
49+
@Test
50+
fun `setTime overrides current time`() {
51+
val provider = FakeTimeProvider(Instant.fromEpochMilliseconds(0L))
52+
val newTime = Instant.fromEpochMilliseconds(999_999L)
53+
54+
provider.setTime(newTime)
55+
56+
assertEquals(newTime, provider.now())
57+
assertEquals(999_999L, provider.currentTimeMillis())
58+
}
59+
60+
@Test
61+
fun `advanceBy works after setTime`() {
62+
val provider = FakeTimeProvider(Instant.fromEpochMilliseconds(0L))
63+
64+
provider.setTime(Instant.fromEpochMilliseconds(10_000L))
65+
provider.advanceBy(500.milliseconds)
66+
67+
assertEquals(10_500L, provider.currentTimeMillis())
68+
}
69+
}

apps/flipcash/features/advanced/src/main/kotlin/com/flipcash/app/advanced/internal/AdvancedFeaturesScreenViewModel.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.flipcash.app.featureflags.FeatureFlagController
77
import com.flipcash.app.menu.MenuItem
88
import com.flipcash.services.user.UserManager
99
import com.getcode.util.resources.ResourceHelper
10+
import com.flipcash.libs.coroutines.DispatcherProvider
1011
import com.getcode.view.BaseViewModel2
1112
import dagger.hilt.android.lifecycle.HiltViewModel
1213
import kotlinx.coroutines.flow.combine
@@ -24,9 +25,11 @@ private val FullMenuList = buildList {
2425
internal class AdvancedFeaturesScreenViewModel @Inject constructor(
2526
userManager: UserManager,
2627
featureFlagController: FeatureFlagController,
28+
dispatchers: DispatcherProvider,
2729
) : BaseViewModel2<AdvancedFeaturesScreenViewModel.State, AdvancedFeaturesScreenViewModel.Event>(
2830
initialState = State(),
29-
updateStateForEvent = updateStateForEvent
31+
updateStateForEvent = updateStateForEvent,
32+
defaultDispatcher = dispatchers.Default,
3033
) {
3134
data class State(
3235
val isBetaEnabled: Boolean = false,

apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceViewModel.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.flipcash.app.core.AppRoute
55
import com.flipcash.services.internal.model.thirdparty.OnRampProvider
66
import com.flipcash.services.user.AuthState
77
import com.flipcash.services.user.UserManager
8+
import com.flipcash.libs.coroutines.DispatcherProvider
89
import com.getcode.view.BaseViewModel2
910
import dagger.hilt.android.lifecycle.HiltViewModel
1011
import kotlinx.coroutines.flow.filter
@@ -17,9 +18,11 @@ import javax.inject.Inject
1718
@HiltViewModel
1819
internal class BalanceViewModel @Inject constructor(
1920
userManager: UserManager,
21+
dispatchers: DispatcherProvider,
2022
) : BaseViewModel2<BalanceViewModel.State, BalanceViewModel.Event>(
2123
initialState = State(),
22-
updateStateForEvent = updateStateForEvent
24+
updateStateForEvent = updateStateForEvent,
25+
defaultDispatcher = dispatchers.Default,
2326
) {
2427
data class State(
2528
val preferredOnRampProvider: OnRampProvider? = null,

apps/flipcash/features/cash/src/main/kotlin/com/flipcash/app/cash/internal/CashScreenViewModel.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import com.flipcash.services.internal.model.thirdparty.OnRampProvider
1515
import com.flipcash.services.internal.model.thirdparty.OnRampType
1616
import com.getcode.manager.BottomBarAction
1717
import com.getcode.manager.BottomBarManager
18-
import com.getcode.opencode.controllers.TransactionController
18+
import com.getcode.opencode.controllers.TransactionOperations
1919
import com.getcode.opencode.exchange.Exchange
2020
import com.getcode.opencode.model.financial.Currency
2121
import com.getcode.opencode.model.financial.CurrencyCode
@@ -29,6 +29,7 @@ import com.getcode.solana.keys.Mint
2929
import com.getcode.ui.components.text.AmountAnimatedInputUiModel
3030
import com.getcode.ui.components.text.NumberInputHelper
3131
import com.getcode.util.resources.ResourceHelper
32+
import com.flipcash.libs.coroutines.DispatcherProvider
3233
import com.getcode.view.BaseViewModel2
3334
import com.getcode.view.LoadingSuccessState
3435
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -52,12 +53,14 @@ internal class CashScreenViewModel @Inject constructor(
5253
private val resources: ResourceHelper,
5354
private val exchange: Exchange,
5455
tokenCoordinator: TokenCoordinator,
55-
transactionController: TransactionController,
56+
transactionController: TransactionOperations,
5657
onrampController: OnRampAmountController,
5758
analytics: FlipcashAnalyticsService,
59+
dispatchers: DispatcherProvider,
5860
) : BaseViewModel2<CashScreenViewModel.State, CashScreenViewModel.Event>(
5961
initialState = State(),
60-
updateStateForEvent = updateStateForEvent
62+
updateStateForEvent = updateStateForEvent,
63+
defaultDispatcher = dispatchers.Default,
6164
) {
6265

6366
private val numberInputHelper = NumberInputHelper()

apps/flipcash/features/contact-verification/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ android {
77
}
88

99
dependencies {
10+
testImplementation(kotlin("test"))
11+
testImplementation(libs.bundles.unit.testing)
12+
testImplementation(libs.mockito.kotlin)
13+
testImplementation(project(":libs:test-utils"))
14+
1015
implementation(libs.compose.activities)
1116

1217
implementation(libs.kotlinx.serialization.json)

0 commit comments

Comments
 (0)