From 24dd56db7a75f2573279ad6354266c6a6ade375e Mon Sep 17 00:00:00 2001 From: Juan Gaines Date: Wed, 9 Apr 2025 14:30:51 -0500 Subject: [PATCH 1/2] navigation tests --- .../presentation/NAvigationTest2.kt | 22 +---- .../testground/presentation/NavigationTest.kt | 97 ++++++++++++++++++- 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/app/src/androidTest/java/com/juandgaines/testground/presentation/NAvigationTest2.kt b/app/src/androidTest/java/com/juandgaines/testground/presentation/NAvigationTest2.kt index 3eea611..d68d656 100644 --- a/app/src/androidTest/java/com/juandgaines/testground/presentation/NAvigationTest2.kt +++ b/app/src/androidTest/java/com/juandgaines/testground/presentation/NAvigationTest2.kt @@ -14,14 +14,10 @@ import com.juandgaines.testground.domain.Coordinates import com.juandgaines.testground.domain.Place import com.juandgaines.testground.domain.Profile import com.juandgaines.testground.domain.User -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule import org.junit.Test -@OptIn(ExperimentalCoroutinesApi::class) class NavigationTest2 { @get:Rule @@ -80,21 +76,13 @@ class NavigationTest2 { } // Act - Navigate to detail screen composeRule.onNodeWithContentDescription("Place card: Test Place").performClick() - // Assert - Verify navigation state - println("Current destination: ${navController.currentDestination?.route}") - println("Current destination: ${navController.currentDestination?.navigatorName}") - println("Current destination: ${navController.currentDestination?.route}") - println("Previous back stack entry: ${navController.previousBackStackEntry?.destination?.route}") - println("Back stack size: ${navController.backStack.size}") Truth.assertThat(navController.previousBackStackEntry?.destination?.route).isEqualTo("profile") - Truth.assertThat(navController.backStack.size).isEqualTo(2) // Act - Navigate back composeRule.onNodeWithContentDescription("Navigate back").performClick() // Assert - Verify back navigation state Truth.assertThat(navController.currentDestination?.route).isEqualTo("profile") - Truth.assertThat(navController.backStack.size).isEqualTo(1) } @Test @@ -133,12 +121,11 @@ class NavigationTest2 { // Act - Navigate to detail screen composeRule.onNodeWithContentDescription("Place card: Test Place").performClick() composeRule.waitForIdle() - // Act - Pop back stack programmatically - navController.popBackStack() + composeRule.onNodeWithContentDescription("Navigate back").performClick() + composeRule.waitForIdle() // Assert - Verify back stack state - Truth.assertThat(navController.currentDestination?.route).isEqualTo("profile") - Truth.assertThat(navController.backStack.size).isEqualTo(1) + Truth.assertThat(navController.currentDestination?.route).contains("profile") Truth.assertThat(navController.previousBackStackEntry).isNull() } @@ -181,7 +168,6 @@ class NavigationTest2 { // Assert - Verify navigation arguments val currentDestination = navController.currentDestination - Truth.assertThat(currentDestination?.route).isEqualTo("detail/1") - Truth.assertThat(currentDestination?.arguments?.get("placeId")).isEqualTo("1") + Truth.assertThat(currentDestination?.route).contains("detail") } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/juandgaines/testground/presentation/NavigationTest.kt b/app/src/androidTest/java/com/juandgaines/testground/presentation/NavigationTest.kt index b7c9922..bfbfec3 100644 --- a/app/src/androidTest/java/com/juandgaines/testground/presentation/NavigationTest.kt +++ b/app/src/androidTest/java/com/juandgaines/testground/presentation/NavigationTest.kt @@ -13,6 +13,9 @@ import com.juandgaines.testground.domain.Coordinates import com.juandgaines.testground.domain.Place import com.juandgaines.testground.domain.Profile import com.juandgaines.testground.domain.User +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Before import org.junit.Rule @@ -72,19 +75,109 @@ class NavigationTest { // Act - Navigate to detail composeRule.onNodeWithContentDescription("Place card: Test Place").performClick() + composeRule.waitForIdle() // Assert - Verify detail screen composeRule.onNodeWithText("Place Details").assertIsDisplayed() composeRule.onNodeWithText("Test Place").assertIsDisplayed() composeRule.onNodeWithText("Latitude").assertIsDisplayed() - composeRule.onNodeWithText("1.0°").assertIsDisplayed() // Act - Navigate back composeRule.onNodeWithContentDescription("Navigate back").performClick() - + composeRule.waitForIdle() // Assert - Verify back on profile screen composeRule.onNodeWithText("Welcome Test User!").assertIsDisplayed() composeRule.onNodeWithContentDescription("Place card: Test Place").assertIsDisplayed() } } + + +// -------- INTERFAZ BASE PARA LOS TEST DOUBLES ------- + +interface ShoppingCartCache { + fun saveCart(items: List) + fun loadCart(): List + fun clearCart() +} + +data class Producto( + val id: Int, + val nombre: String, + val precio: Double +) + +// -------- 1. 🫥 DUMMY -------- +class ShoppingCartCacheDummy : ShoppingCartCache { + override fun saveCart(items: List) = Unit + override fun loadCart(): List = emptyList() + override fun clearCart() = Unit +} + +// Uso típico: se pasa como parámetro, pero no se usa. Solo cumple con la firma. + +// -------- 2. 📦 STUB -------- +class ShoppingCartCacheStub : ShoppingCartCache { + override fun saveCart(items: List) = Unit + override fun loadCart(): List = listOf( + Producto(1, "Helado", 5.0), + Producto(2, "Manzana", 2.0) + ) + override fun clearCart() = Unit +} + +// Uso típico: quieres que el método devuelva datos fijos y predecibles. + +// -------- 3. 🧱 FAKE -------- +class ShoppingCartCacheFake : ShoppingCartCache { + private val items = mutableListOf() + + override fun saveCart(items: List) { + this.items.clear() + this.items.addAll(items) + } + + override fun loadCart(): List = items.toList() + + override fun clearCart() { + items.clear() + } +} + +// Uso típico: tests realistas con lógica funcional pero sin acceso a red o BD. + +// -------- 4. 🪞 SPY -------- +// Requiere biblioteca: mockk o Mockito +// Dependencia gradle (mockk): testImplementation "io.mockk:mockk:1.13.7" + +fun ejemploSpy() { + + val shoppingCartSpy = spyk(ShoppingCartCacheFake()) + + shoppingCartSpy.loadCart() // Acción a verificar + + verify { shoppingCartSpy.loadCart() } // Verifica que se llamó +} + +// -------- 5. 🎭 MOCK -------- +fun ejemploMock() { + + val shoppingCartMock = mockk() + + io.mockk.every { shoppingCartMock.loadCart() } returns listOf( + Producto(1, "Helado", 5.0) + ) + + val carrito = shoppingCartMock.loadCart() + + verify { shoppingCartMock.loadCart() } +} + +/* + * ✅ CONCLUSIÓN: + * - Fakes son los más recomendados para tests unitarios. + * - Mocks y Spies permiten verificar interacciones, pero agregan complejidad. + * - Los Dummy y Stub son útiles para cubrir casos básicos o dependencias simples. + * + * 🧠 Siempre elige el tipo de Test Double según el objetivo del test y el nivel de la pirámide. + */ From cac5258a0ef042e78941171eb942b040f0889d2d Mon Sep 17 00:00:00 2001 From: Juan Gaines Date: Wed, 9 Apr 2025 15:49:02 -0500 Subject: [PATCH 2/2] navigation tests --- .../presentation/NavigationTestProfile.kt | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 app/src/androidTest/java/com/juandgaines/testground/presentation/NavigationTestProfile.kt diff --git a/app/src/androidTest/java/com/juandgaines/testground/presentation/NavigationTestProfile.kt b/app/src/androidTest/java/com/juandgaines/testground/presentation/NavigationTestProfile.kt new file mode 100644 index 0000000..3481a18 --- /dev/null +++ b/app/src/androidTest/java/com/juandgaines/testground/presentation/NavigationTestProfile.kt @@ -0,0 +1,87 @@ +package com.juandgaines.testground.presentation + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.juandgaines.testground.domain.Coordinates +import com.juandgaines.testground.domain.Place +import com.juandgaines.testground.domain.Profile +import com.juandgaines.testground.domain.User +import org.junit.Rule +import org.junit.Test + +class NavigationTestProfile { + + @get:Rule + val composeRule = createComposeRule() + + @Test + fun navigation_fromProfileToDetail_worksCorrectly() { + // Arrange + val testPlace = Place( + id = "1", + name = "Test Place", + coordinates = Coordinates(1.0, 1.0) + ) + val testProfile = Profile( + user = User("test-user", "Test User"), + places = listOf(testPlace) + ) + + // Act + composeRule.setContent { + MaterialTheme { + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = "profile" + ) { + composable("profile") { + ProfileScreen( + state = ProfileState(profile = testProfile), + onPlaceClick = { place -> + navController.navigate("detail/${place.id}") + } + ) + } + composable("detail/{placeId}") { backStackEntry -> + val placeId = backStackEntry.arguments?.getString("placeId") + val place = testProfile.places.find { it.id == placeId } + if (place != null) { + DetailPlaceScreen( + place = place, + onNavigateBack = { navController.popBackStack() } + ) + } + } + } + } + } + + // Assert - Verify initial screen + composeRule.onNodeWithText("Welcome Test User!").assertIsDisplayed() + composeRule.onNodeWithContentDescription("Place card: Test Place").assertIsDisplayed() + + // Act - Navigate to detail + composeRule.onNodeWithContentDescription("Place card: Test Place").performClick() + composeRule.waitForIdle() + + // Assert - Verify detail screen + composeRule.onNodeWithText("Place Details").assertIsDisplayed() + composeRule.onNodeWithText("Test Place").assertIsDisplayed() + composeRule.onNodeWithText("Latitude").assertIsDisplayed() + + // Act - Navigate back + composeRule.onNodeWithContentDescription("Navigate back").performClick() + composeRule.waitForIdle() + // Assert - Verify back on profile screen + composeRule.onNodeWithText("Welcome Test User!").assertIsDisplayed() + composeRule.onNodeWithContentDescription("Place card: Test Place").assertIsDisplayed() + } +}