From 3f71387c090e4c124cd1924dd1943965ed30f3e4 Mon Sep 17 00:00:00 2001 From: Tyler-Lopez <77797048+Tyler-Lopez@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:02:43 -0700 Subject: [PATCH] Add LoginViewModelTest This commit adds unit testing for LoginViewModel. This commit also moves the coroutine from the View to the ViewModel, but using `viewModelScope` in place of `rememberCoroutineScope`. --- app/build.gradle.kts | 1 + .../example/mvvm/ui/login/ui/loginScreen.kt | 8 +-- .../mvvm/ui/login/ui/loginViewModel.kt | 20 +++--- .../mvvm/ui/login/ui/LoginViewModelTest.kt | 67 +++++++++++++++++++ gradle/libs.versions.toml | 2 + 5 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 app/src/test/java/com/example/mvvm/ui/login/ui/LoginViewModelTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9c8b76e..09a6f85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.foundation.layout) testImplementation(libs.junit) + testImplementation(libs.robolectric) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/java/com/example/mvvm/ui/login/ui/loginScreen.kt b/app/src/main/java/com/example/mvvm/ui/login/ui/loginScreen.kt index 8929e4d..edd509e 100644 --- a/app/src/main/java/com/example/mvvm/ui/login/ui/loginScreen.kt +++ b/app/src/main/java/com/example/mvvm/ui/login/ui/loginScreen.kt @@ -60,7 +60,6 @@ fun Login( val password by viewModel.password.collectAsStateWithLifecycle() val enabled by viewModel.enabled.collectAsStateWithLifecycle() val loading by viewModel.loading.collectAsStateWithLifecycle() - val coroutine = rememberCoroutineScope() if (loading) { Box(Modifier.fillMaxSize()) { @@ -78,11 +77,8 @@ fun Login( Spacer(modifier = Modifier.padding((8.dp))) ForgotPassword(Modifier.align(Alignment.End)) // column organiza a lo que hay debajo, la propiedad align() de los hijos de una Column espera un Alignment.Horizontal, no un Alignment completo (que es bidimensional). Spacer(modifier = Modifier.padding((16.dp))) - LoginButton(enabled ) { - coroutine.launch { - viewModel.pressButton() - - } + LoginButton(enabled) { + viewModel.pressButton() } } diff --git a/app/src/main/java/com/example/mvvm/ui/login/ui/loginViewModel.kt b/app/src/main/java/com/example/mvvm/ui/login/ui/loginViewModel.kt index e7e9451..9a09cce 100644 --- a/app/src/main/java/com/example/mvvm/ui/login/ui/loginViewModel.kt +++ b/app/src/main/java/com/example/mvvm/ui/login/ui/loginViewModel.kt @@ -1,11 +1,14 @@ package com.example.mvvm.ui.login.ui import android.util.Patterns +import androidx.annotation.VisibleForTesting import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch class LoginViewModel : ViewModel() { private val _email = MutableStateFlow("") // crea un flujo mutable @@ -29,15 +32,14 @@ class LoginViewModel : ViewModel() { } + @VisibleForTesting fun isValidEmail(email: String): Boolean = Patterns.EMAIL_ADDRESS.matcher(email).matches() + @VisibleForTesting fun isValidPassword(password: String): Boolean = password.length > 6 - fun isValidEmail(email: String): Boolean = Patterns.EMAIL_ADDRESS.matcher(email).matches() - fun isValidPassword(password: String): Boolean = password.length > 6 - - suspend fun pressButton() { - _loading.value = true - delay(4000) - _loading.value = false + fun pressButton() { + viewModelScope.launch { + _loading.value = true + delay(4000) + _loading.value = false + } } - - } diff --git a/app/src/test/java/com/example/mvvm/ui/login/ui/LoginViewModelTest.kt b/app/src/test/java/com/example/mvvm/ui/login/ui/LoginViewModelTest.kt new file mode 100644 index 0000000..d8abb5a --- /dev/null +++ b/app/src/test/java/com/example/mvvm/ui/login/ui/LoginViewModelTest.kt @@ -0,0 +1,67 @@ +package com.example.mvvm.ui.login.ui + +import org.junit.Before +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LoginViewModelTest { + private lateinit var viewModel: LoginViewModel + + @Before + fun setup() { + viewModel = LoginViewModel() + } + + @Test + fun `email and password are empty on launch`() { + assert(viewModel.email.value.isEmpty()) + assert(viewModel.password.value.isEmpty()) + } + + @Test + fun `login button is not enabled on launch`() { + assert(!viewModel.enabled.value) + } + + @Test + fun `the password value updates when the event is received`() { + val newPassword = "myValidPassword" + viewModel.onLoginChanged( + newEmail = "", // notice this is awkward to test when coupled + newPassword = newPassword, + ) + assertEquals(newPassword, viewModel.password.value) + } + + @Test + fun `a non-valid e-mail is not considered valid`() { + val emailIsValidExpected = false + val emailIsValidActual = viewModel.isValidEmail("bloop") + + assertEquals(emailIsValidExpected, emailIsValidActual) + } + + @Test + fun `a valid e-mail is considered valid`() { + val emailIsValidExpected = true + val emailIsValidActual = viewModel.isValidEmail("bloop@gmail.com") + + assertEquals(emailIsValidExpected, emailIsValidActual) + } + + @Test + fun `when an email and password are entered, the button to submit is enabled`() { + val newEmail = "myValidEmail@gmail.com" + val newPassword = "myValidPassword" + + viewModel.onLoginChanged( + newEmail = newEmail, + newPassword = newPassword, + ) + + assert(viewModel.enabled.value) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 18cd69b..2f8d8f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.8.0" composeBom = "2024.09.00" foundationLayout = "1.9.4" +robolectric = "4.16" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -26,6 +27,7 @@ androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui- androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" } +robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }