Skip to content

Commit 81f5983

Browse files
committed
Refactor ViewModel tests to utilize ViewModelTestBase for consistent main dispatcher handling across desktop and Android environments. Update test methods in Admin, Auth, ChangePassword, Plugins, Profile, Security, and Settings ViewModel tests to improve test execution reliability. Enhance documentation in TESTING.md to clarify testing setup and coverage reporting.
1 parent d93c220 commit 81f5983

14 files changed

Lines changed: 136 additions & 53 deletions

File tree

compose-frontend/composeApp/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ android {
159159
}
160160
}
161161

162+
// ViewModel and UploadManager tests use UnconfinedTestDispatcher for Main (ViewModelTestBase)
163+
// so they run on desktop and Android without a real main looper.
164+
162165
// Make sure compilation tasks depend on KSP metadata generation
163166
tasks.withType<KotlinCompilationTask<*>>().configureEach {
164167
if (name != "kspCommonMainKotlinMetadata") {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Helpers for ViewModel and UploadManager tests that use viewModelScope (Main dispatcher).
3+
* Set Dispatchers.Main to UnconfinedTestDispatcher so tests pass on desktop and Android
4+
* without requiring a real main looper. Use withMainDispatcher() or runTestWithMain() in each test.
5+
*/
6+
7+
package com.vaultstadio.app.feature
8+
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.ExperimentalCoroutinesApi
11+
import kotlinx.coroutines.test.TestScope
12+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
13+
import kotlinx.coroutines.test.resetMain
14+
import kotlinx.coroutines.test.runTest
15+
import kotlinx.coroutines.test.setMain
16+
17+
@OptIn(ExperimentalCoroutinesApi::class)
18+
object ViewModelTestBase {
19+
20+
private val testDispatcher = UnconfinedTestDispatcher()
21+
22+
/** Run a synchronous test with Main set to UnconfinedTestDispatcher; resets Main after. */
23+
fun withMainDispatcher(block: () -> Unit) {
24+
Dispatchers.setMain(testDispatcher)
25+
try {
26+
block()
27+
} finally {
28+
Dispatchers.resetMain()
29+
}
30+
}
31+
32+
/** Run a coroutine test with Main set to UnconfinedTestDispatcher; resets Main after. */
33+
fun runTestWithMain(block: suspend TestScope.() -> Unit) = runTest {
34+
Dispatchers.setMain(testDispatcher)
35+
try {
36+
block()
37+
} finally {
38+
Dispatchers.resetMain()
39+
}
40+
}
41+
}

compose-frontend/composeApp/src/commonTest/kotlin/com/vaultstadio/app/feature/admin/AdminViewModelTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import com.vaultstadio.app.domain.usecase.admin.GetAdminUsersUseCase
1414
import com.vaultstadio.app.domain.usecase.admin.UpdateUserQuotaUseCase
1515
import com.vaultstadio.app.domain.usecase.admin.UpdateUserRoleUseCase
1616
import com.vaultstadio.app.domain.usecase.admin.UpdateUserStatusUseCase
17-
import kotlinx.coroutines.test.runTest
17+
import com.vaultstadio.app.feature.ViewModelTestBase
1818
import kotlinx.datetime.Instant
1919
import kotlin.test.Test
2020
import kotlin.test.assertEquals
@@ -76,7 +76,7 @@ class AdminViewModelTest {
7676
)
7777

7878
@Test
79-
fun clearError_clearsErrorMessage() = runTest {
79+
fun clearError_clearsErrorMessage() = ViewModelTestBase.runTestWithMain {
8080
val vm = createViewModel(getUsersResult = ApiResult.error("ERR", "Something failed"))
8181
vm.loadUsers()
8282
testScheduler.advanceUntilIdle()
@@ -86,7 +86,7 @@ class AdminViewModelTest {
8686
}
8787

8888
@Test
89-
fun loadUsers_onSuccess_updatesUsers() = runTest {
89+
fun loadUsers_onSuccess_updatesUsers() = ViewModelTestBase.runTestWithMain {
9090
val users = listOf(testAdminUser())
9191
val vm = createViewModel(
9292
getUsersResult = ApiResult.success(PaginatedResponse(users, 1L, 0, 50, 1, false)),
@@ -98,7 +98,7 @@ class AdminViewModelTest {
9898
}
9999

100100
@Test
101-
fun clearError_doesNotThrow() {
101+
fun clearError_doesNotThrow() = ViewModelTestBase.withMainDispatcher {
102102
val vm = createViewModel()
103103
vm.clearError()
104104
assertNull(vm.error)

compose-frontend/composeApp/src/commonTest/kotlin/com/vaultstadio/app/feature/auth/AuthViewModelTest.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package com.vaultstadio.app.feature.auth
77

8+
import com.vaultstadio.app.feature.ViewModelTestBase
89
import com.vaultstadio.app.data.network.ApiResult
910
import com.vaultstadio.app.data.repository.AuthRepository
1011
import com.vaultstadio.app.domain.model.LoginResult
@@ -90,39 +91,39 @@ class AuthViewModelTest {
9091
}
9192

9293
@Test
93-
fun updateLoginEmail_updatesState() {
94+
fun updateLoginEmail_updatesState() = ViewModelTestBase.withMainDispatcher {
9495
val vm = createViewModel()
9596
assertEquals("", vm.loginEmail)
9697
vm.updateLoginEmail("a@b.com")
9798
assertEquals("a@b.com", vm.loginEmail)
9899
}
99100

100101
@Test
101-
fun updateLoginPassword_updatesState() {
102+
fun updateLoginPassword_updatesState() = ViewModelTestBase.withMainDispatcher {
102103
val vm = createViewModel()
103104
assertEquals("", vm.loginPassword)
104105
vm.updateLoginPassword("secret")
105106
assertEquals("secret", vm.loginPassword)
106107
}
107108

108109
@Test
109-
fun login_withBlankEmail_setsEmailPasswordRequired() {
110+
fun login_withBlankEmail_setsEmailPasswordRequired() = ViewModelTestBase.withMainDispatcher {
110111
val vm = createViewModel()
111112
vm.updateLoginPassword("pass")
112113
vm.login()
113114
assertTrue(vm.authError is AuthError.EmailPasswordRequired)
114115
}
115116

116117
@Test
117-
fun login_withBlankPassword_setsEmailPasswordRequired() {
118+
fun login_withBlankPassword_setsEmailPasswordRequired() = ViewModelTestBase.withMainDispatcher {
118119
val vm = createViewModel()
119120
vm.updateLoginEmail("a@b.com")
120121
vm.login()
121122
assertTrue(vm.authError is AuthError.EmailPasswordRequired)
122123
}
123124

124125
@Test
125-
fun register_withPasswordMismatch_setsPasswordsDoNotMatch() {
126+
fun register_withPasswordMismatch_setsPasswordsDoNotMatch() = ViewModelTestBase.withMainDispatcher {
126127
val vm = createViewModel()
127128
vm.updateRegisterEmail("a@b.com")
128129
vm.updateRegisterUsername("user")
@@ -133,7 +134,7 @@ class AuthViewModelTest {
133134
}
134135

135136
@Test
136-
fun register_withShortPassword_setsPasswordTooShort() {
137+
fun register_withShortPassword_setsPasswordTooShort() = ViewModelTestBase.withMainDispatcher {
137138
val vm = createViewModel()
138139
vm.updateRegisterEmail("a@b.com")
139140
vm.updateRegisterUsername("user")
@@ -144,15 +145,15 @@ class AuthViewModelTest {
144145
}
145146

146147
@Test
147-
fun register_withBlankFields_setsAllFieldsRequired() {
148+
fun register_withBlankFields_setsAllFieldsRequired() = ViewModelTestBase.withMainDispatcher {
148149
val vm = createViewModel()
149150
vm.updateRegisterEmail("a@b.com")
150151
vm.register()
151152
assertTrue(vm.authError is AuthError.AllFieldsRequired)
152153
}
153154

154155
@Test
155-
fun toggleRegister_flipsShowRegister() {
156+
fun toggleRegister_flipsShowRegister() = ViewModelTestBase.withMainDispatcher {
156157
val vm = createViewModel()
157158
assertEquals(false, vm.showRegister)
158159
vm.toggleRegister()
@@ -162,7 +163,7 @@ class AuthViewModelTest {
162163
}
163164

164165
@Test
165-
fun clearError_clearsAuthError() {
166+
fun clearError_clearsAuthError() = ViewModelTestBase.withMainDispatcher {
166167
val vm = createViewModel()
167168
vm.updateLoginPassword("x")
168169
vm.login()

compose-frontend/composeApp/src/commonTest/kotlin/com/vaultstadio/app/feature/changepassword/ChangePasswordViewModelTest.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package com.vaultstadio.app.feature.changepassword
77

8+
import com.vaultstadio.app.feature.ViewModelTestBase
89
import com.vaultstadio.app.data.network.ApiResult
910
import com.vaultstadio.app.domain.usecase.auth.ChangePasswordUseCase
1011
import kotlin.test.Test
@@ -26,7 +27,7 @@ class ChangePasswordViewModelTest {
2627
}
2728

2829
@Test
29-
fun updateCurrentPassword_updatesStateAndClearsError() {
30+
fun updateCurrentPassword_updatesStateAndClearsError() = ViewModelTestBase.withMainDispatcher {
3031
val vm = createViewModel()
3132
vm.updateNewPassword("x")
3233
vm.changePassword()
@@ -37,21 +38,21 @@ class ChangePasswordViewModelTest {
3738
}
3839

3940
@Test
40-
fun updateNewPassword_updatesStateAndClearsError() {
41+
fun updateNewPassword_updatesStateAndClearsError() = ViewModelTestBase.withMainDispatcher {
4142
val vm = createViewModel()
4243
vm.updateNewPassword("newpass")
4344
assertEquals("newpass", vm.newPassword)
4445
}
4546

4647
@Test
47-
fun updateConfirmPassword_updatesState() {
48+
fun updateConfirmPassword_updatesState() = ViewModelTestBase.withMainDispatcher {
4849
val vm = createViewModel()
4950
vm.updateConfirmPassword("confirm")
5051
assertEquals("confirm", vm.confirmPassword)
5152
}
5253

5354
@Test
54-
fun changePassword_withBlankCurrent_setsErrorMessage() {
55+
fun changePassword_withBlankCurrent_setsErrorMessage() = ViewModelTestBase.withMainDispatcher {
5556
val vm = createViewModel()
5657
vm.updateNewPassword("newpassword")
5758
vm.updateConfirmPassword("newpassword")
@@ -60,7 +61,7 @@ class ChangePasswordViewModelTest {
6061
}
6162

6263
@Test
63-
fun changePassword_withBlankNew_setsErrorMessage() {
64+
fun changePassword_withBlankNew_setsErrorMessage() = ViewModelTestBase.withMainDispatcher {
6465
val vm = createViewModel()
6566
vm.updateCurrentPassword("current")
6667
vm.updateConfirmPassword("newpassword")
@@ -69,7 +70,7 @@ class ChangePasswordViewModelTest {
6970
}
7071

7172
@Test
72-
fun changePassword_withShortNew_setsErrorMessage() {
73+
fun changePassword_withShortNew_setsErrorMessage() = ViewModelTestBase.withMainDispatcher {
7374
val vm = createViewModel()
7475
vm.updateCurrentPassword("current")
7576
vm.updateNewPassword("short")
@@ -79,7 +80,7 @@ class ChangePasswordViewModelTest {
7980
}
8081

8182
@Test
82-
fun changePassword_whenPasswordsDoNotMatch_setsErrorMessage() {
83+
fun changePassword_whenPasswordsDoNotMatch_setsErrorMessage() = ViewModelTestBase.withMainDispatcher {
8384
val vm = createViewModel()
8485
vm.updateCurrentPassword("current")
8586
vm.updateNewPassword("newpassword")
@@ -89,7 +90,7 @@ class ChangePasswordViewModelTest {
8990
}
9091

9192
@Test
92-
fun changePassword_whenNewSameAsCurrent_setsErrorMessage() {
93+
fun changePassword_whenNewSameAsCurrent_setsErrorMessage() = ViewModelTestBase.withMainDispatcher {
9394
val vm = createViewModel()
9495
vm.updateCurrentPassword("samepass")
9596
vm.updateNewPassword("samepass")
@@ -99,7 +100,7 @@ class ChangePasswordViewModelTest {
99100
}
100101

101102
@Test
102-
fun toggleCurrentPasswordVisibility_flipsVisibility() {
103+
fun toggleCurrentPasswordVisibility_flipsVisibility() = ViewModelTestBase.withMainDispatcher {
103104
val vm = createViewModel()
104105
assertFalse(vm.showCurrentPassword)
105106
vm.toggleCurrentPasswordVisibility()
@@ -109,23 +110,23 @@ class ChangePasswordViewModelTest {
109110
}
110111

111112
@Test
112-
fun toggleNewPasswordVisibility_flipsVisibility() {
113+
fun toggleNewPasswordVisibility_flipsVisibility() = ViewModelTestBase.withMainDispatcher {
113114
val vm = createViewModel()
114115
assertFalse(vm.showNewPassword)
115116
vm.toggleNewPasswordVisibility()
116117
assertTrue(vm.showNewPassword)
117118
}
118119

119120
@Test
120-
fun toggleConfirmPasswordVisibility_flipsVisibility() {
121+
fun toggleConfirmPasswordVisibility_flipsVisibility() = ViewModelTestBase.withMainDispatcher {
121122
val vm = createViewModel()
122123
assertFalse(vm.showConfirmPassword)
123124
vm.toggleConfirmPasswordVisibility()
124125
assertTrue(vm.showConfirmPassword)
125126
}
126127

127128
@Test
128-
fun dismissSuccess_doesNotThrow() {
129+
fun dismissSuccess_doesNotThrow() = ViewModelTestBase.withMainDispatcher {
129130
val vm = createViewModel()
130131
vm.dismissSuccess()
131132
assertFalse(vm.isSuccess)

compose-frontend/composeApp/src/commonTest/kotlin/com/vaultstadio/app/feature/plugins/PluginsViewModelTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package com.vaultstadio.app.feature.plugins
77

8+
import com.vaultstadio.app.feature.ViewModelTestBase
89
import com.vaultstadio.app.data.network.ApiResult
910
import com.vaultstadio.app.domain.model.PluginInfo
1011
import com.vaultstadio.app.domain.usecase.plugin.DisablePluginUseCase
@@ -52,14 +53,14 @@ class PluginsViewModelTest {
5253
)
5354

5455
@Test
55-
fun clearError_doesNotThrow() {
56+
fun clearError_doesNotThrow() = ViewModelTestBase.withMainDispatcher {
5657
val vm = createViewModel()
5758
vm.clearError()
5859
assertNull(vm.error)
5960
}
6061

6162
@Test
62-
fun loadPlugins_doesNotThrow() {
63+
fun loadPlugins_doesNotThrow() = ViewModelTestBase.withMainDispatcher {
6364
val vm = createViewModel()
6465
vm.loadPlugins()
6566
}

compose-frontend/composeApp/src/commonTest/kotlin/com/vaultstadio/app/feature/profile/ProfileViewModelTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package com.vaultstadio.app.feature.profile
77

8+
import com.vaultstadio.app.feature.ViewModelTestBase
89
import com.vaultstadio.app.data.network.ApiResult
910
import com.vaultstadio.app.data.repository.AuthRepository
1011
import com.vaultstadio.app.domain.model.LoginResult
@@ -72,14 +73,14 @@ class ProfileViewModelTest {
7273
}
7374

7475
@Test
75-
fun clearError_clearsError() {
76+
fun clearError_clearsError() = ViewModelTestBase.withMainDispatcher {
7677
val vm = createViewModel()
7778
vm.clearError()
7879
assertNull(vm.error)
7980
}
8081

8182
@Test
82-
fun clearSuccessMessage_clearsSuccessMessage() {
83+
fun clearSuccessMessage_clearsSuccessMessage() = ViewModelTestBase.withMainDispatcher {
8384
val vm = createViewModel()
8485
vm.clearSuccessMessage()
8586
assertNull(vm.successMessage)

compose-frontend/composeApp/src/commonTest/kotlin/com/vaultstadio/app/feature/security/SecurityViewModelTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package com.vaultstadio.app.feature.security
77

8+
import com.vaultstadio.app.feature.ViewModelTestBase
89
import com.vaultstadio.app.data.network.ApiResult
910
import com.vaultstadio.app.domain.model.ActiveSession
1011
import com.vaultstadio.app.domain.model.LoginEvent
@@ -69,7 +70,7 @@ class SecurityViewModelTest {
6970
)
7071

7172
@Test
72-
fun showRevokeDialog_setsSession() {
73+
fun showRevokeDialog_setsSession() = ViewModelTestBase.withMainDispatcher {
7374
val vm = createViewModel()
7475
assertNull(vm.showRevokeSessionDialog)
7576
val session = testSession("s99")
@@ -78,15 +79,15 @@ class SecurityViewModelTest {
7879
}
7980

8081
@Test
81-
fun dismissRevokeDialog_clearsSession() {
82+
fun dismissRevokeDialog_clearsSession() = ViewModelTestBase.withMainDispatcher {
8283
val vm = createViewModel()
8384
vm.showRevokeDialog(testSession())
8485
vm.dismissRevokeDialog()
8586
assertNull(vm.showRevokeSessionDialog)
8687
}
8788

8889
@Test
89-
fun dismissError_clearsErrorMessage() {
90+
fun dismissError_clearsErrorMessage() = ViewModelTestBase.withMainDispatcher {
9091
val vm = createViewModel()
9192
vm.toggleTwoFactor()
9293
assertTrue(vm.errorMessage != null)
@@ -95,7 +96,7 @@ class SecurityViewModelTest {
9596
}
9697

9798
@Test
98-
fun toggleTwoFactor_setsNotAvailableMessage() {
99+
fun toggleTwoFactor_setsNotAvailableMessage() = ViewModelTestBase.withMainDispatcher {
99100
val vm = createViewModel()
100101
vm.toggleTwoFactor()
101102
assertEquals("Two-factor authentication setup is not yet available", vm.errorMessage)

0 commit comments

Comments
 (0)