diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a9b3f42..b227899 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,6 +23,7 @@ android { targetSdk = 34 versionCode = 3 versionName = "1.1.0" + testInstrumentationRunner = "com.manuelnunez.apps.navigation.CustomTestRunner" } signingConfigs { @@ -69,9 +70,10 @@ android { } dependencies { + implementation(projects.core.data) // Added only for testing using the TestDataModule implementation(projects.core.ui) - implementation(projects.core.data) implementation(projects.core.domain) + implementation(projects.features.home.ui) implementation(projects.features.detail.ui) implementation(projects.features.seemore.ui) @@ -86,6 +88,7 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.coil.kt) + implementation(libs.androidx.test.runner) // Compose val composeBom = platform(libs.androidx.compose.bom) @@ -94,4 +97,7 @@ dependencies { // Hilt Dependency Injection implementation(libs.hilt.android) ksp(libs.hilt.compiler) + + androidTestImplementation(libs.androidx.navigation.testing) + androidTestImplementation(libs.hilt.android.testing) } diff --git a/app/src/androidTest/kotlin/com/manuelnunez/apps/navigation/CustomTestRunner.kt b/app/src/androidTest/kotlin/com/manuelnunez/apps/navigation/CustomTestRunner.kt new file mode 100644 index 0000000..4000519 --- /dev/null +++ b/app/src/androidTest/kotlin/com/manuelnunez/apps/navigation/CustomTestRunner.kt @@ -0,0 +1,14 @@ +package com.manuelnunez.apps.navigation + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + +// A custom runner to set up the instrumented application class for tests. +class CustomTestRunner : AndroidJUnitRunner() { + + override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { + return super.newApplication(cl, HiltTestApplication::class.java.name, context) + } +} diff --git a/app/src/androidTest/kotlin/com/manuelnunez/apps/navigation/MainNavigationTest.kt b/app/src/androidTest/kotlin/com/manuelnunez/apps/navigation/MainNavigationTest.kt new file mode 100644 index 0000000..eeece67 --- /dev/null +++ b/app/src/androidTest/kotlin/com/manuelnunez/apps/navigation/MainNavigationTest.kt @@ -0,0 +1,161 @@ +package com.manuelnunez.apps.navigation + +import androidx.annotation.StringRes +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.manuelnunez.apps.MainActivity +import com.manuelnunez.apps.core.ui.component.CustomCardAutomation +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import kotlin.properties.ReadOnlyProperty +import com.manuelnunez.apps.core.ui.R as CoreUIR +import com.manuelnunez.apps.features.detail.ui.R as DetailR +import com.manuelnunez.apps.features.favorites.ui.R as FavorR +import com.manuelnunez.apps.features.home.ui.R as HomeR + +@HiltAndroidTest +class MainNavigationTest { + private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) = + ReadOnlyProperty { _, _ -> activity.getString(resId) } + + @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() + + private val navigateUp by composeTestRule.stringResource(resId = CoreUIR.string.button_back) + private val home by composeTestRule.stringResource(resId = RootScreen.HOME.contentDescription) + private val favorite by + composeTestRule.stringResource(resId = RootScreen.FAVORITES.contentDescription) + private val details by + composeTestRule.stringResource(resId = DetailR.string.section_details_title) + private val featureHomeTitle by composeTestRule.stringResource(HomeR.string.section_feature) + private val featureFavoriteTitle by + composeTestRule.stringResource(FavorR.string.section_favorites) + private val featureDetailFavoriteButton by + composeTestRule.stringResource(DetailR.string.button_favorite) + + @Before + fun init() { + hiltRule.inject() + } + + @Test + fun firstTabScreen_startDestination_isHomeNotArrowShown() { + composeTestRule.apply { + // Tab Home selected + onNodeWithContentDescription(home).assertIsSelected() + onNodeWithContentDescription(favorite).assertIsNotSelected() + + // HomeTitle + onNodeWithContentDescription(featureHomeTitle).assertExists() + + // GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown. + onNodeWithContentDescription(navigateUp).assertDoesNotExist() + } + } + + @Test + fun secondTabScreen_navigateFromHome_isFavoriteNotArrowShown() { + composeTestRule.apply { + // Navigate to tab favorites + onNodeWithContentDescription(favorite).performClick().assertIsSelected() + + // Favorite title + onNodeWithContentDescription(featureFavoriteTitle).assertExists() + + // Home tab not selected + onNodeWithContentDescription(home).assertIsNotSelected() + + // GIVEN the user is on any of the top level destinations, THEN the Up arrow is not shown. + onNodeWithContentDescription(navigateUp).assertDoesNotExist() + } + } + + @Test + fun detailScreen_navigateFromHome_onClickAnyPhoto() { + composeTestRule.apply { + // Home tab selected + onNodeWithContentDescription(home).assertIsSelected() + + // Click on any photo and navigate to details + onAllNodesWithTag(CustomCardAutomation.IMAGE_CARD_PREFIX).onFirst().performClick() + + // We are in Details Screen + onNodeWithContentDescription(details).performClick() + + // GIVEN the user is not on any top level destinations, THEN the Up arrow is shown. + onNodeWithContentDescription(navigateUp).assertExists().performClick() + + // We are in HomeScreen again + onNodeWithContentDescription(featureHomeTitle).assertExists() + } + } + + @Test + fun detailScreen_navigateFromFavorite_onClickAnyPhoto() { + composeTestRule.apply { + // Home tab selected + onNodeWithContentDescription(home).assertIsSelected() + addItemToFavorites() + + // Move to tab favorites + onNodeWithContentDescription(favorite).performClick() + onNodeWithContentDescription(featureFavoriteTitle).assertExists() + + // Click on any photo and navigate to details + onAllNodesWithTag(CustomCardAutomation.IMAGE_CARD_PREFIX).onFirst().performClick() + + // We are in Details Screen + onNodeWithContentDescription(details).performClick() + + // GIVEN the user is not on any top level destinations, THEN the Up arrow is shown. + onNodeWithContentDescription(navigateUp).assertExists().performClick() + + // We are in HomeScreen again + onNodeWithContentDescription(featureFavoriteTitle).assertExists() + } + } + + @Test + fun navigationBar_reselectTab_keepsState() { + composeTestRule.apply { + // Home tab selected + onNodeWithContentDescription(home).assertIsSelected() + + // Click on any photo and navigate to details + onAllNodesWithTag(CustomCardAutomation.IMAGE_CARD_PREFIX).onFirst().performClick() + + // We are in Details Screen + onNodeWithContentDescription(details).performClick() + + // Move to tab favorites + onNodeWithContentDescription(favorite).performClick() + + // Move back to home + onNodeWithContentDescription(home).performClick() + + // We are stills in Details Screen + onNodeWithContentDescription(details).performClick() + } + } + + private fun AndroidComposeTestRule, MainActivity> + .addItemToFavorites() { + // Click on any photo and navigate to details + onAllNodesWithTag(CustomCardAutomation.IMAGE_CARD_PREFIX).onFirst().performClick() + // Add to favorites + onNodeWithContentDescription(featureDetailFavoriteButton).performClick() + // Back + onNodeWithContentDescription(navigateUp).assertExists().performClick() + } +} diff --git a/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt b/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt index 67299bf..72e69d0 100644 --- a/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt +++ b/app/src/main/kotlin/com/manuelnunez/apps/MainApp.kt @@ -23,6 +23,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.navigation.NavController import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.compose.rememberNavController @@ -71,7 +73,10 @@ fun MainApp() { private fun MainBottomNavBar(navController: NavController, currentSelectedScreen: RootScreen) { MainNavigationBar { mainDestinations().forEach { rootScreen -> + val contentDescriptionScreen = stringResource(id = rootScreen.contentDescription) + MainNavigationBarItem( + modifier = Modifier.semantics { contentDescription = contentDescriptionScreen }, selected = currentSelectedScreen == rootScreen, onClick = { navController.navigateToRootScreen(rootScreen) }, icon = { @@ -95,7 +100,9 @@ private fun MainBottomNavBar(navController: NavController, currentSelectedScreen private fun MainNavRail(navController: NavController, currentSelectedScreen: RootScreen) { MainNavigationRail { mainDestinations().forEach { rootScreen -> + val contentDescriptionScreen = stringResource(id = rootScreen.contentDescription) MainNavigationRailItem( + modifier = Modifier.semantics { contentDescription = contentDescriptionScreen }, selected = currentSelectedScreen == rootScreen, onClick = { navController.navigateToRootScreen(rootScreen) }, icon = { diff --git a/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt b/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt index f3765aa..8b15311 100644 --- a/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt +++ b/app/src/main/kotlin/com/manuelnunez/apps/navigation/MainNavigation.kt @@ -63,19 +63,22 @@ enum class RootScreen( val unselectedIcon: ImageVector, val iconTextId: Int, val titleTextId: Int, + val contentDescription: Int ) { HOME( route = "home_root", selectedIcon = Icons.Default.Home, unselectedIcon = Icons.Outlined.Home, iconTextId = R.string.home_destination_title, - titleTextId = R.string.home_destination_title), + titleTextId = R.string.home_destination_title, + contentDescription = R.string.home_destination_content_description), FAVORITES( route = "favorite_root", selectedIcon = Icons.Default.Favorite, unselectedIcon = Icons.Outlined.FavoriteBorder, iconTextId = R.string.favorites_destination_title, - titleTextId = R.string.favorites_destination_title) + titleTextId = R.string.favorites_destination_title, + contentDescription = R.string.favorites_destination_content_description) } fun NavController.navigateToRootScreen(rootScreen: RootScreen) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d5d7b69..a63b999 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,7 @@ Purrfect Pics Home + HomeTabItem Favorites + FavoritesTabItem \ No newline at end of file diff --git a/core/common/src/main/kotlin/com/manuelnunez/apps/core/common/test/MockkAllRule.kt b/core/common/src/main/kotlin/com/manuelnunez/apps/core/common/testRule/MockkAllRule.kt similarity index 94% rename from core/common/src/main/kotlin/com/manuelnunez/apps/core/common/test/MockkAllRule.kt rename to core/common/src/main/kotlin/com/manuelnunez/apps/core/common/testRule/MockkAllRule.kt index 3e3047c..ffe8948 100644 --- a/core/common/src/main/kotlin/com/manuelnunez/apps/core/common/test/MockkAllRule.kt +++ b/core/common/src/main/kotlin/com/manuelnunez/apps/core/common/testRule/MockkAllRule.kt @@ -1,4 +1,4 @@ -package com.manuelnunez.apps.core.common.test +package com.manuelnunez.apps.core.common.testRule import com.manuelnunez.apps.core.common.dispatcher.CoroutineDispatcherProvider import io.mockk.unmockkAll @@ -14,7 +14,7 @@ import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext -@ExperimentalCoroutinesApi +@OptIn(ExperimentalCoroutinesApi::class) class MockkAllRule : BeforeEachCallback, AfterEachCallback { private val testCoroutinesDispatcher = StandardTestDispatcher() diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index f9c612a..2fd66f5 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -25,8 +25,10 @@ android { dependencies { implementation(projects.core.common) implementation(projects.core.services) - implementation(projects.core.domain) + implementation( + projects.core.domain) // Added only for the domain Item model. It could be moved to common implementation(projects.core.datastoreProto) + // Added features domain only for the repository interface implementation(projects.features.home.domain) implementation(projects.features.seemore.domain) implementation(projects.features.favorites.domain) @@ -40,6 +42,7 @@ dependencies { implementation(libs.androidx.paging.common.ktx) // HILT + implementation(libs.hilt.android.testing) implementation(libs.hilt.android) ksp(libs.hilt.compiler) diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/TestDataModule.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/TestDataModule.kt new file mode 100644 index 0000000..e56562d --- /dev/null +++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/di/TestDataModule.kt @@ -0,0 +1,44 @@ +package com.manuelnunez.apps.core.data.di + +import com.manuelnunez.apps.core.data.repository.fake.FakeDetailRepository +import com.manuelnunez.apps.core.data.repository.fake.FakeFavoritesRepository +import com.manuelnunez.apps.core.data.repository.fake.FakeHomeRepository +import com.manuelnunez.apps.core.data.repository.fake.FakeSeeMoreRepository +import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository +import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository +import com.manuelnunez.apps.features.home.domain.repository.HomeRepository +import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import javax.inject.Singleton + +/** + * This Test Module is used as replace of the DataModule when testing is done. In case of the + * MainNavigationTest, this avoid to call network and emit fake data instead. + */ +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [DataModule::class], +) +internal interface TestDataModule { + @Singleton + @Binds + abstract fun bindsHomeRepository(homeRepository: FakeHomeRepository): HomeRepository + + @Singleton + @Binds + abstract fun bindsSeeMoreRepository(seeMoreRepository: FakeSeeMoreRepository): SeeMoreRepository + + @Singleton + @Binds + abstract fun bindsDetailRepository(detailRepository: FakeDetailRepository): DetailRepository + + @Singleton + @Binds + abstract fun bindsFavoritesRepository( + seeMoreRepository: FakeFavoritesRepository + ): FavoritesRepository +} diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeDetailRepository.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeDetailRepository.kt new file mode 100644 index 0000000..eea261e --- /dev/null +++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeDetailRepository.kt @@ -0,0 +1,21 @@ +package com.manuelnunez.apps.core.data.repository.fake + +import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class FakeDetailRepository @Inject constructor() : DetailRepository { + private val items = mutableListOf() + + override suspend fun saveFavoriteItem(favoriteItem: Item) { + items.add(favoriteItem) + } + + override suspend fun removeFavoriteItem(favoriteItem: Item) { + items.remove(favoriteItem) + } + + override fun isItemFavorite(itemPhotoId: String): Flow = flowOf(true) +} diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeFavoritesRepository.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeFavoritesRepository.kt new file mode 100644 index 0000000..d2d17b9 --- /dev/null +++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeFavoritesRepository.kt @@ -0,0 +1,12 @@ +package com.manuelnunez.apps.core.data.repository.fake + +import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.domain.utils.mockItems +import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class FakeFavoritesRepository @Inject constructor() : FavoritesRepository { + override fun getAllFavorites(): Flow> = flowOf(mockItems) +} diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeHomeRepository.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeHomeRepository.kt new file mode 100644 index 0000000..bbb2ce8 --- /dev/null +++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeHomeRepository.kt @@ -0,0 +1,13 @@ +package com.manuelnunez.apps.core.data.repository.fake + +import com.manuelnunez.apps.core.common.eitherSuccess +import com.manuelnunez.apps.core.domain.utils.mockItems +import com.manuelnunez.apps.features.home.domain.repository.HomeRepository +import javax.inject.Inject + +class FakeHomeRepository @Inject constructor() : HomeRepository { + + override fun getPopularItems() = eitherSuccess(mockItems.shuffled().take(10)) + + override fun getFeaturedItems() = eitherSuccess(mockItems.shuffled().take(5)) +} diff --git a/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeSeeMoreRepository.kt b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeSeeMoreRepository.kt new file mode 100644 index 0000000..7f2f2b8 --- /dev/null +++ b/core/data/src/main/kotlin/com/manuelnunez/apps/core/data/repository/fake/FakeSeeMoreRepository.kt @@ -0,0 +1,12 @@ +package com.manuelnunez.apps.core.data.repository.fake + +import androidx.paging.PagingData +import com.manuelnunez.apps.core.domain.utils.mockItems +import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +class FakeSeeMoreRepository @Inject constructor() : SeeMoreRepository { + + override fun getAllItems() = flowOf(PagingData.Companion.from(mockItems)) +} diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSourceTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSourceTest.kt new file mode 100644 index 0000000..a4d2b65 --- /dev/null +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/local/FavoritesDataSourceTest.kt @@ -0,0 +1,49 @@ +package com.manuelnunez.apps.core.data.datasource.local + +import androidx.datastore.core.DataStore +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule +import com.manuelnunez.apps.core.datastore.proto.ItemList +import com.manuelnunez.apps.core.domain.model.Item +import io.mockk.coVerify +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class FavoritesDataSourceTest { + @RegisterExtension private val mockkAllExtension = MockkAllRule() + @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() + + val dataStore: DataStore = mockk(relaxed = true) + val dataSource = FavoritesDataSource(dataStore) + + @Test + fun `addItemToFavorites adds new item to favorites`() = + mockkAllExtension.runTest { + val newItem = Item("1", "image-url", "thumbnail-url", "description") + + // When + dataSource.addItemToFavorites(newItem) + + // Then + dataSource.favorites.collect { assertTrue(it.any { it.photoId == newItem.photoId }) } + coVerify(exactly = 1) { dataStore.updateData(any()) } + } + + @Test + fun `remoteItemFromFavorites remove item from favorites`() = + mockkAllExtension.runTest { + val removeThisItem = Item("1", "image-url", "thumbnail-url", "description") + + // When + dataSource.addItemToFavorites(removeThisItem) + dataSource.removeItemFromFavorites(removeThisItem.photoId) + + // Then + dataSource.favorites.collect { + assertTrue(it.none { it.photoId == removeThisItem.photoId }) + } + coVerify(exactly = 2) { dataStore.updateData(any()) } // one add, one remove + } +} diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/remote/CataasCatsRemoteDataSourceTest.kt similarity index 94% rename from core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt rename to core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/remote/CataasCatsRemoteDataSourceTest.kt index 73ff980..d1c788e 100644 --- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/CataasCatsRemoteDataSourceTest.kt +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/remote/CataasCatsRemoteDataSourceTest.kt @@ -1,4 +1,4 @@ -package com.manuelnunez.apps.core.data.datasource +package com.manuelnunez.apps.core.data.datasource.remote import androidx.paging.PagingData import androidx.paging.map @@ -6,8 +6,6 @@ import app.cash.turbine.test import com.manuelnunez.apps.core.common.Either import com.manuelnunez.apps.core.common.eitherError import com.manuelnunez.apps.core.common.eitherSuccess -import com.manuelnunez.apps.core.data.datasource.remote.CataasCatsRemoteDataSource -import com.manuelnunez.apps.core.data.datasource.remote.CataasCatsRemoteDataSourceImpl import com.manuelnunez.apps.core.data.mapper.toItems import com.manuelnunez.apps.core.data.utils.mockCataasResponseDTOS import com.manuelnunez.apps.core.domain.model.Item @@ -17,12 +15,14 @@ import com.manuelnunez.apps.core.services.executors.ServiceError import com.manuelnunez.apps.core.services.executors.ServicesExecutor import com.manuelnunez.apps.core.services.executors.toServiceResponse import com.manuelnunez.apps.core.services.service.CataasService +import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -40,6 +40,11 @@ class CataasCatsRemoteDataSourceTest { remoteDataSource = CataasCatsRemoteDataSourceImpl(servicesExecutor, apiService) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `when getItems is called successfully, then returns PexelsSearchResponseDTO`() { val responseSuccess = Response.success(mockCataasResponseDTOS) diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/remote/PexelsCatsRemoteDataSourceTest.kt similarity index 94% rename from core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt rename to core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/remote/PexelsCatsRemoteDataSourceTest.kt index a0f7c0b..d7ffcb1 100644 --- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/PexelsCatsRemoteDataSourceTest.kt +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/datasource/remote/PexelsCatsRemoteDataSourceTest.kt @@ -1,4 +1,4 @@ -package com.manuelnunez.apps.core.data.datasource +package com.manuelnunez.apps.core.data.datasource.remote import androidx.paging.PagingData import androidx.paging.map @@ -6,8 +6,6 @@ import app.cash.turbine.test import com.manuelnunez.apps.core.common.Either import com.manuelnunez.apps.core.common.eitherError import com.manuelnunez.apps.core.common.eitherSuccess -import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSource -import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSourceImpl import com.manuelnunez.apps.core.data.mapper.toItems import com.manuelnunez.apps.core.data.utils.mockPexelsSearchResponseDTO import com.manuelnunez.apps.core.domain.model.Item @@ -17,12 +15,14 @@ import com.manuelnunez.apps.core.services.executors.ServiceError import com.manuelnunez.apps.core.services.executors.ServicesExecutor import com.manuelnunez.apps.core.services.executors.toServiceResponse import com.manuelnunez.apps.core.services.service.PexelsService +import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach @@ -41,6 +41,11 @@ class PexelsCatsRemoteDataSourceTest { remoteDataSource = PexelsCatsRemoteDataSourceImpl(servicesExecutor, apiService) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `when getItems is called successfully, then returns PexelsSearchResponseDTO`() { val responseSuccess = Response.success(mockPexelsSearchResponseDTO) diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryTest.kt index 0fd6eb2..24a4a5f 100644 --- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryTest.kt +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/DetailRepositoryTest.kt @@ -1,8 +1,9 @@ package com.manuelnunez.apps.core.data.repository import com.manuelnunez.apps.core.data.datasource.local.FavoritesDataSource -import com.manuelnunez.apps.core.data.utils.mockItems +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository +import io.mockk.clearAllMocks import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.confirmVerified @@ -11,6 +12,7 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -24,6 +26,11 @@ class DetailRepositoryTest { repository = DetailRepositoryImpl(remoteDataSource) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `call to saveFavoriteItem invokes addItemToFavorites from datasource`() = runTest { coJustRun { remoteDataSource.addItemToFavorites(mockItems[0]) } diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryTest.kt index c044591..de901f9 100644 --- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryTest.kt +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/FavoritesRepositoryTest.kt @@ -1,13 +1,15 @@ package com.manuelnunez.apps.core.data.repository import com.manuelnunez.apps.core.data.datasource.local.FavoritesDataSource -import com.manuelnunez.apps.core.data.utils.mockItems +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository +import io.mockk.clearAllMocks import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.flow +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -21,6 +23,11 @@ class FavoritesRepositoryTest { repository = FavoritesRepositoryImpl(remoteDataSource) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `call to getAllFavorites returns item data from datasource`() { every { remoteDataSource.favorites } returns flow { emit(mockItems) } diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryTest.kt index aba354f..27ca67d 100644 --- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryTest.kt +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/HomeRepositoryTest.kt @@ -9,8 +9,10 @@ import com.manuelnunez.apps.core.data.utils.mockPexelsSearchResponseDTO import com.manuelnunez.apps.core.domain.model.ErrorModel import com.manuelnunez.apps.core.services.executors.ServiceError import com.manuelnunez.apps.features.home.domain.repository.HomeRepository +import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach @@ -26,6 +28,11 @@ class HomeRepositoryTest { repository = HomeRepositoryImpl(remoteDataSource) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `GIVEN getPopularItems call, WHEN success, THEN return 10 shuffled items`() { every { remoteDataSource.getItems() } returns eitherSuccess(mockPexelsSearchResponseDTO) diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt index c7e2b07..a114921 100644 --- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/repository/SeeMoreRepositoryTest.kt @@ -2,14 +2,16 @@ package com.manuelnunez.apps.core.data.repository import androidx.paging.PagingData import com.manuelnunez.apps.core.data.datasource.remote.PexelsCatsRemoteDataSource -import com.manuelnunez.apps.core.data.utils.mockItems +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository +import io.mockk.clearAllMocks import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -24,6 +26,11 @@ class SeeMoreRepositoryTest { repository = SeeMoreRepositoryImpl(remoteDataSource) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `GIVEN getAllItems call, WHEN success, THEN return items`() { val expectedResult = PagingData.from(mockItems) diff --git a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/utils/MockUtils.kt b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/utils/Mocks.kt similarity index 92% rename from core/data/src/test/kotlin/com/manuelnunez/apps/core/data/utils/MockUtils.kt rename to core/data/src/test/kotlin/com/manuelnunez/apps/core/data/utils/Mocks.kt index a8a79c1..d48209a 100644 --- a/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/utils/MockUtils.kt +++ b/core/data/src/test/kotlin/com/manuelnunez/apps/core/data/utils/Mocks.kt @@ -1,12 +1,11 @@ package com.manuelnunez.apps.core.data.utils -import com.manuelnunez.apps.core.domain.model.Item import com.manuelnunez.apps.core.services.dto.CataasResponseDTO import com.manuelnunez.apps.core.services.dto.PexelsSearchResponseDTO import com.manuelnunez.apps.core.services.dto.PhotoDTO import com.manuelnunez.apps.core.services.dto.PhotoSrcDTO -val mockPhotos: List = +val mockPhotosDTO: List = List(20) { index -> val id = index + 1 val photographer = if (id % 2 == 0) "John Doe" else "Jane Smith" @@ -38,7 +37,7 @@ val mockPhotos: List = } val mockPexelsSearchResponseDTO = - PexelsSearchResponseDTO(totalResult = 10, page = 1, perPage = 20, photos = mockPhotos) + PexelsSearchResponseDTO(totalResult = 10, page = 1, perPage = 20, photos = mockPhotosDTO) val mockCataasResponseDTOS = listOf( @@ -57,5 +56,3 @@ val mockCataasResponseDTOS = mimetype = "image/jpeg", size = 4096, tags = listOf("funny", "mischief", "whiskers"))) - -val mockItems = listOf(Item("1", "imageUrl1", "thumbnailUrl1", "description1")) diff --git a/core/datastore-proto/build.gradle.kts b/core/datastore-proto/build.gradle.kts index dc35c6c..b1877eb 100644 --- a/core/datastore-proto/build.gradle.kts +++ b/core/datastore-proto/build.gradle.kts @@ -17,6 +17,8 @@ android { defaultConfig { minSdk = 21 } kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } + + tasks.withType { useJUnitPlatform() } } // Setup protobuf configuration, generating lite Java and Kotlin classes @@ -33,10 +35,14 @@ protobuf { } dependencies { + implementation(projects.core.common) + implementation(libs.protobuf.kotlin.lite) implementation(libs.androidx.dataStore.core) // HILT implementation(libs.hilt.android) ksp(libs.hilt.compiler) + + testImplementation(libs.junit) } diff --git a/core/datastore-proto/src/test/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/ItemListSerializerTest.kt b/core/datastore-proto/src/test/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/ItemListSerializerTest.kt new file mode 100644 index 0000000..47db6fe --- /dev/null +++ b/core/datastore-proto/src/test/kotlin/com/manuelnunez/apps/core/datastore/proto/serializer/ItemListSerializerTest.kt @@ -0,0 +1,53 @@ +package com.manuelnunez.apps.core.datastore.proto.serializer + +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule +import com.manuelnunez.apps.core.datastore.proto.ItemList +import com.manuelnunez.apps.core.datastore.proto.itemList +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream + +class ItemListSerializerTest { + @RegisterExtension private val mockkAllExtension = MockkAllRule() + @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() + + private val serializer = ItemListSerializer() + + @Test + fun `defaultItemList is empty for default`() { + assertEquals( + itemList { + // Default value + }, + serializer.defaultValue, + ) + } + + @Test + fun `read and write ItemList correctly from InputStream`() = + mockkAllExtension.runTest { + // Given + val expectedItemList = ItemList.getDefaultInstance() + val outputStream = ByteArrayOutputStream() + val inputStream = ByteArrayInputStream(outputStream.toByteArray()) + + // When + serializer.writeTo(expectedItemList, outputStream) + val actualItemList = serializer.readFrom(inputStream) + + // Then + assertEquals(expectedItemList, actualItemList) + } + + @Test + fun `readFrom throws IllegalArgumentException when parsing fails`() { + // When, Then + assertThrows(IllegalArgumentException::class.java) { + mockkAllExtension.runTest { serializer.readFrom(ByteArrayInputStream(byteArrayOf(0))) } + } + } +} diff --git a/core/domain/src/main/kotlin/com/manuelnunez/apps/core/domain/utils/Mocks.kt b/core/domain/src/main/kotlin/com/manuelnunez/apps/core/domain/utils/Mocks.kt new file mode 100644 index 0000000..29d703f --- /dev/null +++ b/core/domain/src/main/kotlin/com/manuelnunez/apps/core/domain/utils/Mocks.kt @@ -0,0 +1,17 @@ +package com.manuelnunez.apps.core.domain.utils + +import com.manuelnunez.apps.core.domain.model.Item + +val mockItems: List = + List(20) { index -> + val id = (index + 1).toString() + Item( + photoId = id, + imageUrl = "https://example.com/photo$id", + thumbnailUrl = "https://example.com/photo$id/small", + description = "This is a description for item $id") + } + +val mockPopularPhotos = mockItems.shuffled().take(10) + +val mockFeaturedPhotos = mockItems.shuffled().take(5) diff --git a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCaseTest.kt b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCaseTest.kt index aceecd1..cef6f80 100644 --- a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCaseTest.kt +++ b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/GetFavoriteStatusUseCaseTest.kt @@ -1,20 +1,21 @@ package com.manuelnunez.apps.features.detail.domain.usecase import app.cash.turbine.test -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository +import io.mockk.clearAllMocks import io.mockk.coVerify import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -@OptIn(ExperimentalCoroutinesApi::class) class GetFavoriteStatusUseCaseTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() @@ -29,13 +30,18 @@ class GetFavoriteStatusUseCaseTest { detailRepository, mockkAllExtension.testCoroutineDispatcherProvider) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `call GetFavoriteStatusUseCase invokes isItemFavorite from repository`() = mockkAllExtension.runTest { - every { detailRepository.isItemFavorite(mockItem.photoId) } returns flow { emit(false) } - useCase.prepare(mockItem.photoId).test {} + every { detailRepository.isItemFavorite(mockItems[0].photoId) } returns flow { emit(false) } + useCase.prepare(mockItems[0].photoId).test {} - coVerify(exactly = 1) { detailRepository.isItemFavorite(mockItem.photoId) } + coVerify(exactly = 1) { detailRepository.isItemFavorite(mockItems[0].photoId) } confirmVerified(detailRepository) } } diff --git a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/MockUtils.kt b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/MockUtils.kt deleted file mode 100644 index 860e1ab..0000000 --- a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/MockUtils.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.manuelnunez.apps.features.detail.domain.usecase - -import com.manuelnunez.apps.core.domain.model.Item - -val mockItem = - Item( - "1324", - "https://example.com/1324", - description = "description: 1324", - thumbnailUrl = "https://example.com/1324") diff --git a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCaseTest.kt b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCaseTest.kt index d3689d7..e8dec65 100644 --- a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCaseTest.kt +++ b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/RemoveFavoritesUseCaseTest.kt @@ -1,18 +1,19 @@ package com.manuelnunez.apps.features.detail.domain.usecase import app.cash.turbine.test -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository +import io.mockk.clearAllMocks import io.mockk.coVerify import io.mockk.confirmVerified import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -@OptIn(ExperimentalCoroutinesApi::class) class RemoveFavoritesUseCaseTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() @@ -26,12 +27,17 @@ class RemoveFavoritesUseCaseTest { RemoveFavoritesUseCase(detailRepository, mockkAllExtension.testCoroutineDispatcherProvider) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `call RemoveFavoritesUseCase invokes removeFavoriteItem from repository`() = mockkAllExtension.runTest { - useCase.prepare(mockItem).test {} + useCase.prepare(mockItems[0]).test {} - coVerify(exactly = 1) { detailRepository.removeFavoriteItem(mockItem) } + coVerify(exactly = 1) { detailRepository.removeFavoriteItem(mockItems[0]) } confirmVerified(detailRepository) } } diff --git a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCaseTest.kt b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCaseTest.kt index 65ba9ec..d016cf1 100644 --- a/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCaseTest.kt +++ b/features/detail/domain/src/test/kotlin/com/manuelnunez/apps/features/detail/domain/usecase/SaveFavoritesUseCaseTest.kt @@ -1,18 +1,19 @@ package com.manuelnunez.apps.features.detail.domain.usecase import app.cash.turbine.test -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.detail.domain.repository.DetailRepository +import io.mockk.clearAllMocks import io.mockk.coVerify import io.mockk.confirmVerified import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -@OptIn(ExperimentalCoroutinesApi::class) class SaveFavoritesUseCaseTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() @@ -26,12 +27,17 @@ class SaveFavoritesUseCaseTest { SaveFavoritesUseCase(detailRepository, mockkAllExtension.testCoroutineDispatcherProvider) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `call SaveFavoritesUseCase invokes saveFavoriteItem from repository`() = mockkAllExtension.runTest { - useCase.prepare(mockItem).test {} + useCase.prepare(mockItems[0]).test {} - coVerify(exactly = 1) { detailRepository.saveFavoriteItem(mockItem) } + coVerify(exactly = 1) { detailRepository.saveFavoriteItem(mockItems[0]) } confirmVerified(detailRepository) } } diff --git a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailScreenTest.kt b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailScreenTest.kt index a7fb1f3..19ba223 100644 --- a/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailScreenTest.kt +++ b/features/detail/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/detail/ui/DetailScreenTest.kt @@ -16,7 +16,7 @@ class DetailScreenTest { @get:Rule(order = 0) val composeTestRule = createAndroidComposeRule() @Test - fun photo_whenScreenIsLoaded_showsPhotoShareAndDescription() { + fun photo_whenScreenIsLoaded_showsPhotoShareAddToFavoriteAndDescription() { composeTestRule.setContent { DetailScreen(item = mockItem, isFavorite = false, onBackClick = {}, onFavoriteClicked = {}) } @@ -41,6 +41,15 @@ class DetailScreenTest { .assertExists() .assertHasClickAction() + // favorite button + composeTestRule + .onNodeWithContentDescription( + composeTestRule.activity.resources.getString(R.string.button_favorite), + substring = true, + ) + .assertExists() + .assertHasClickAction() + // Image composeTestRule .onNodeWithContentDescription(mockItem.photoId) @@ -50,8 +59,8 @@ class DetailScreenTest { private val mockItem = Item( - photoId = "14gf", - imageUrl = "https://example.com/photo14gf", - thumbnailUrl = "https://example.com/photo14gf/small", - description = "This is a description for popular items 14gf") + photoId = "id", + imageUrl = "https://example.com/photoid", + thumbnailUrl = "https://example.com/photoid/small", + description = "This is a description for item id") } diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModel.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModel.kt index c157a0e..2db8fec 100644 --- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModel.kt +++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModel.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn +import org.jetbrains.annotations.VisibleForTesting import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @@ -41,7 +42,7 @@ constructor( .stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = Item.empty) - private val isItemFavorite = MutableStateFlow(false) + @VisibleForTesting val isItemFavorite = MutableStateFlow(false) val state = combine(isItemFavorite, selectedItem) { isFavorite, item -> DetailUiState(item, isFavorite) } diff --git a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt index 8e12e30..a6a00d5 100644 --- a/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt +++ b/features/detail/ui/src/main/kotlin/com/manuelnunez/apps/features/detail/ui/components/DetailScreen.kt @@ -77,7 +77,7 @@ private fun DetailPortrait( modifier = Modifier.weight(1f).wrapContentSize(), horizontalAlignment = Alignment.CenterHorizontally) { StatefulAsyncImage( - modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp).heightIn(min = 180.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp), imageUrl = item.imageUrl, contentDescription = item.photoId, contentScale = ContentScale.Fit, diff --git a/features/detail/ui/src/test/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModelTest.kt b/features/detail/ui/src/test/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModelTest.kt new file mode 100644 index 0000000..13c2a67 --- /dev/null +++ b/features/detail/ui/src/test/kotlin/com/manuelnunez/apps/features/detail/ui/DetailViewModelTest.kt @@ -0,0 +1,121 @@ +package com.manuelnunez.apps.features.detail.ui + +import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule +import com.manuelnunez.apps.core.domain.model.toEncodedString +import com.manuelnunez.apps.core.domain.utils.mockItems +import com.manuelnunez.apps.features.detail.domain.usecase.GetFavoriteStatusUseCase +import com.manuelnunez.apps.features.detail.domain.usecase.RemoveFavoritesUseCase +import com.manuelnunez.apps.features.detail.domain.usecase.SaveFavoritesUseCase +import com.manuelnunez.apps.features.detail.ui.navigation.DETAIL_ITEM +import io.mockk.clearAllMocks +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import kotlin.properties.Delegates + +class DetailViewModelTest { + @RegisterExtension private val mockkAllExtension = MockkAllRule() + @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() + + private val saveFavoritesUseCase = mockk() + private val removeFavoritesUseCase = mockk() + private val getFavoriteStatusUseCase = mockk() + private val savedStateHandle = mockk() + private val selectedItemStringFlow = MutableStateFlow(mockItems[0].toEncodedString()) + + private var viewModel: DetailViewModel by Delegates.notNull() + + @BeforeEach + fun setup() { + every { savedStateHandle.getStateFlow(DETAIL_ITEM, "") } returns selectedItemStringFlow + + every { getFavoriteStatusUseCase.prepare(any()) } returns flow { emit(true) } + + viewModel = + DetailViewModel( + saveFavoritesUseCase, + removeFavoritesUseCase, + getFavoriteStatusUseCase, + savedStateHandle) + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + @Test + fun `on Init is called checkFavorite and sets isItemFavorite correctly`() = + mockkAllExtension.runTest { + val isFavorite = true + + viewModel.state.test { + // GIVEN viewModel INIT + + assertEquals(DetailViewModel.DetailUiState.Empty, awaitItem()) + assertEquals( + DetailViewModel.DetailUiState(isFavorite = isFavorite, item = mockItems[0]), + awaitItem()) + + cancelAndIgnoreRemainingEvents() + } + + verify(exactly = 1) { getFavoriteStatusUseCase.prepare(any()) } + confirmVerified(getFavoriteStatusUseCase) + } + + @Test + fun `favorite calls removeFavorite if isItemFavorite is true`() { + // Given + every { removeFavoritesUseCase.prepare(any()) } returns flow { emit(Unit) } + + viewModel = + DetailViewModel( + saveFavoritesUseCase, + removeFavoritesUseCase, + getFavoriteStatusUseCase, + savedStateHandle) + + viewModel.isItemFavorite.value = true + + // When + viewModel.favorite() + + // Then + verify(exactly = 1) { viewModel.removeFavorite(any()) } + verify(exactly = 0) { viewModel.saveFavorite(any()) } + } + + @Test + fun `favorite calls saveFavorites if isItemFavorite is false`() { + // Given + every { saveFavoritesUseCase.prepare(any()) } returns flow { emit(Unit) } + + viewModel = + DetailViewModel( + saveFavoritesUseCase, + removeFavoritesUseCase, + getFavoriteStatusUseCase, + savedStateHandle) + + viewModel.isItemFavorite.value = false + + // When + viewModel.favorite() + + // Then + verify(exactly = 0) { viewModel.removeFavorite(any()) } + verify(exactly = 1) { viewModel.saveFavorite(any()) } + } +} diff --git a/features/favorites/domain/src/test/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCaseTest.kt b/features/favorites/domain/src/test/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCaseTest.kt index c9e2d17..04d28b6 100644 --- a/features/favorites/domain/src/test/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCaseTest.kt +++ b/features/favorites/domain/src/test/kotlin/com/manuelnunez/apps/features/favorites/domain/usecase/GetFavoritesUseCaseTest.kt @@ -1,21 +1,21 @@ package com.manuelnunez.apps.features.favorites.domain.usecase import app.cash.turbine.test -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule -import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.favorites.domain.repository.FavoritesRepository +import io.mockk.clearAllMocks import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -@OptIn(ExperimentalCoroutinesApi::class) class GetFavoritesUseCaseTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() @@ -29,6 +29,11 @@ class GetFavoritesUseCaseTest { GetFavoritesUseCase(favoritesRepository, mockkAllExtension.testCoroutineDispatcherProvider) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `call GetFavoritesUseCase invokes getAllFavorites from repository`() = mockkAllExtension.runTest { @@ -38,13 +43,4 @@ class GetFavoritesUseCaseTest { verify(exactly = 1) { favoritesRepository.getAllFavorites() } confirmVerified(favoritesRepository) } - - private val mockItems = - List(5) { index -> - Item( - "$index", - "https://example.com/$index", - description = "description: $index", - thumbnailUrl = "https://example.com/$index") - } } diff --git a/features/favorites/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesScreenTest.kt b/features/favorites/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesScreenTest.kt index 9e8a6a9..ed3035e 100644 --- a/features/favorites/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesScreenTest.kt +++ b/features/favorites/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesScreenTest.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText -import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.favorites.ui.component.FavoritesScreen import org.junit.Rule import org.junit.Test @@ -36,13 +36,4 @@ class FavoritesScreenTest { .assertExists() .assertHasClickAction() } - - private val mockItems = - List(5) { index -> - Item( - "$index", - "https://example.com/$index", - description = "description: $index", - thumbnailUrl = "https://example.com/$index") - } } diff --git a/features/favorites/ui/src/test/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModelTest.kt b/features/favorites/ui/src/test/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModelTest.kt index dee54aa..0797e4f 100644 --- a/features/favorites/ui/src/test/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModelTest.kt +++ b/features/favorites/ui/src/test/kotlin/com/manuelnunez/apps/features/favorites/ui/FavoritesViewModelTest.kt @@ -1,16 +1,15 @@ package com.manuelnunez.apps.features.favorites.ui import app.cash.turbine.test -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule -import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.favorites.domain.usecase.GetFavoritesUseCase import com.manuelnunez.apps.features.favorites.ui.FavoritesViewModel.FavoriteItemsState import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -18,7 +17,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import kotlin.properties.Delegates -@OptIn(ExperimentalCoroutinesApi::class) class FavoritesViewModelTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() @@ -30,7 +28,7 @@ class FavoritesViewModelTest { @Test fun `GIVEN viewmodel init, WHEN onSuccess, THEN set state with favorite items`() = mockkAllExtension.runTest { - every { getFavoritesUseCase.prepare(Unit) } returns flow { emit(mockPhotos) } + every { getFavoritesUseCase.prepare(Unit) } returns flow { emit(mockItems) } viewModel = FavoritesViewModel(getFavoritesUseCase) @@ -39,7 +37,7 @@ class FavoritesViewModelTest { assertTrue(awaitItem() is FavoriteItemsState.Idle) assertTrue(awaitItem() is FavoriteItemsState.Loading) - assertEquals(FavoriteItemsState.ShowList(mockPhotos), awaitItem()) + assertEquals(FavoriteItemsState.ShowList(mockItems), awaitItem()) cancelAndIgnoreRemainingEvents() } @@ -68,14 +66,4 @@ class FavoritesViewModelTest { verify(exactly = 1) { getFavoritesUseCase.prepare(Unit) } confirmVerified(getFavoritesUseCase) } - - private val mockPhotos: List = - List(20) { index -> - val id = (index + 1).toString() - Item( - photoId = id, - imageUrl = "https://example.com/photo$id", - thumbnailUrl = "https://example.com/photo$id/small", - description = "This is a description for item $id") - } } diff --git a/features/home/domain/src/test/kotlin/com/manuelnunez/apps/features/home/domain/usecase/GetFeaturedItemsUseCaseTest.kt b/features/home/domain/src/test/kotlin/com/manuelnunez/apps/features/home/domain/usecase/GetFeaturedItemsUseCaseTest.kt index 85d1962..39d7004 100644 --- a/features/home/domain/src/test/kotlin/com/manuelnunez/apps/features/home/domain/usecase/GetFeaturedItemsUseCaseTest.kt +++ b/features/home/domain/src/test/kotlin/com/manuelnunez/apps/features/home/domain/usecase/GetFeaturedItemsUseCaseTest.kt @@ -1,18 +1,18 @@ package com.manuelnunez.apps.features.home.domain.usecase import app.cash.turbine.test -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule import com.manuelnunez.apps.features.home.domain.repository.HomeRepository +import io.mockk.clearAllMocks import io.mockk.confirmVerified import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -@OptIn(ExperimentalCoroutinesApi::class) class GetFeaturedItemsUseCaseTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() @@ -26,6 +26,11 @@ class GetFeaturedItemsUseCaseTest { GetFeaturedItemsUseCase(homeRepository, mockkAllExtension.testCoroutineDispatcherProvider) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `call GetFeaturedItemsUseCase invokes getFeatureItems from repository`() = mockkAllExtension.runTest { diff --git a/features/home/domain/src/test/kotlin/com/manuelnunez/apps/features/home/domain/usecase/GetPopularItemsUseCaseTest.kt b/features/home/domain/src/test/kotlin/com/manuelnunez/apps/features/home/domain/usecase/GetPopularItemsUseCaseTest.kt index 97b31ab..945e08d 100644 --- a/features/home/domain/src/test/kotlin/com/manuelnunez/apps/features/home/domain/usecase/GetPopularItemsUseCaseTest.kt +++ b/features/home/domain/src/test/kotlin/com/manuelnunez/apps/features/home/domain/usecase/GetPopularItemsUseCaseTest.kt @@ -1,18 +1,18 @@ package com.manuelnunez.apps.features.home.domain.usecase import app.cash.turbine.test -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule import com.manuelnunez.apps.features.home.domain.repository.HomeRepository +import io.mockk.clearAllMocks import io.mockk.confirmVerified import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -@OptIn(ExperimentalCoroutinesApi::class) class GetPopularItemsUseCaseTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() @@ -26,6 +26,11 @@ class GetPopularItemsUseCaseTest { GetPopularItemsUseCase(homeRepository, mockkAllExtension.testCoroutineDispatcherProvider) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `call GetItemUseCase invokes getFeatureItems from repository`() = mockkAllExtension.runTest { diff --git a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenTest.kt b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenTest.kt index d3f107e..add8bc1 100644 --- a/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenTest.kt +++ b/features/home/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/home/ui/HomeScreenTest.kt @@ -5,7 +5,8 @@ import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText -import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.domain.utils.mockFeaturedPhotos +import com.manuelnunez.apps.core.domain.utils.mockPopularPhotos import com.manuelnunez.apps.features.home.ui.HomeViewModel.FeaturedItemsState import com.manuelnunez.apps.features.home.ui.HomeViewModel.HomeUiState import com.manuelnunez.apps.features.home.ui.HomeViewModel.PopularItemsState @@ -141,28 +142,4 @@ class HomeScreenTest { ) .assertExists() } - - private val mockPopularPhotos = - List(20) { index -> - val id = (index + 1).toString() - Item( - photoId = id, - imageUrl = "https://example.com/photo$id", - thumbnailUrl = "https://example.com/photo$id/small", - description = "This is a description for popular items $id") - } - .shuffled() - .take(10) - - private val mockFeaturedPhotos = - List(20) { index -> - val id = (index + 1).toString() - Item( - photoId = id, - imageUrl = "https://example.com/photo$id", - thumbnailUrl = "https://example.com/photo$id/small", - description = "This is a description for featured items $id") - } - .shuffled() - .take(5) } diff --git a/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt b/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt index d42c4f8..d9b1780 100644 --- a/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt +++ b/features/home/ui/src/test/kotlin/com/manuelnunez/apps/features/home/ui/viewmodel/HomeViewModelTest.kt @@ -3,10 +3,10 @@ package com.manuelnunez.apps.features.home.ui.viewmodel import app.cash.turbine.test import com.manuelnunez.apps.core.common.eitherError import com.manuelnunez.apps.core.common.eitherSuccess -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule import com.manuelnunez.apps.core.domain.model.ErrorModel -import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.home.domain.usecase.GetFeaturedItemsUseCase import com.manuelnunez.apps.features.home.domain.usecase.GetPopularItemsUseCase import com.manuelnunez.apps.features.home.ui.HomeViewModel @@ -16,7 +16,6 @@ import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -24,7 +23,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import kotlin.properties.Delegates -@OptIn(ExperimentalCoroutinesApi::class) class HomeViewModelTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() @@ -38,9 +36,9 @@ class HomeViewModelTest { fun `GIVEN viewmodel init, WHEN onSuccess, THEN set state with popular and featured items`() = mockkAllExtension.runTest { every { getFeaturedItemsUseCase.prepare(Unit) } returns - flow { emit(eitherSuccess(mockPhotos)) } + flow { emit(eitherSuccess(mockItems)) } every { getPopularItemsUseCase.prepare(Unit) } returns - flow { emit(eitherSuccess(mockPhotos)) } + flow { emit(eitherSuccess(mockItems)) } viewModel = HomeViewModel(getFeaturedItemsUseCase, getPopularItemsUseCase) @@ -53,8 +51,8 @@ class HomeViewModelTest { } awaitItem().apply { - assertEquals(FeaturedItemsState.ShowList(mockPhotos), featuredItemsState) - assertEquals(PopularItemsState.ShowList(mockPhotos), popularItemsState) + assertEquals(FeaturedItemsState.ShowList(mockItems), featuredItemsState) + assertEquals(PopularItemsState.ShowList(mockItems), popularItemsState) } cancelAndIgnoreRemainingEvents() @@ -131,14 +129,4 @@ class HomeViewModelTest { } confirmVerified(getFeaturedItemsUseCase, getPopularItemsUseCase) } - - private val mockPhotos: List = - List(20) { index -> - val id = (index + 1).toString() - Item( - photoId = id, - imageUrl = "https://example.com/photo$id", - thumbnailUrl = "https://example.com/photo$id/small", - description = "This is a description for item $id") - } } diff --git a/features/seemore/domain/src/test/kotlin/com/manuelnunez/apps/feature/seemore/domain/GetAllItemUseCaseTest.kt b/features/seemore/domain/src/test/kotlin/com/manuelnunez/apps/feature/seemore/domain/GetAllItemUseCaseTest.kt index 789a4d2..0dffe0d 100644 --- a/features/seemore/domain/src/test/kotlin/com/manuelnunez/apps/feature/seemore/domain/GetAllItemUseCaseTest.kt +++ b/features/seemore/domain/src/test/kotlin/com/manuelnunez/apps/feature/seemore/domain/GetAllItemUseCaseTest.kt @@ -2,21 +2,21 @@ package com.manuelnunez.apps.features.seemore.domain import androidx.paging.PagingData import app.cash.turbine.test -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule import com.manuelnunez.apps.features.seemore.domain.repository.SeeMoreRepository import com.manuelnunez.apps.features.seemore.domain.usecase.GetAllItemUseCase +import io.mockk.clearAllMocks import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -@OptIn(ExperimentalCoroutinesApi::class) class GetAllItemUseCaseTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @RegisterExtension private val unMockkAllExtension = UnMockkAllRule() @@ -30,6 +30,11 @@ class GetAllItemUseCaseTest { GetAllItemUseCase(seeMoreRepository, mockkAllExtension.testCoroutineDispatcherProvider) } + @AfterEach + fun tearDown() { + clearAllMocks() + } + @Test fun `call GetItemUseCase invokes getFeatureItems from repository`() = mockkAllExtension.runTest { diff --git a/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreScreenTest.kt b/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreScreenTest.kt index 267a48f..393ecc0 100644 --- a/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreScreenTest.kt +++ b/features/seemore/ui/src/androidTest/kotlin/com/manuelnunez/apps/features/seemore/ui/SeeMoreScreenTest.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems -import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.seemore.ui.components.SeeMoreErrorScreen import com.manuelnunez.apps.features.seemore.ui.components.SeeMoreScreen import kotlinx.coroutines.flow.flowOf @@ -57,13 +57,4 @@ class SeeMoreScreenTest { ) .assertExists() } - - private val mockItems = - List(5) { index -> - Item( - "$index", - "https://example.com/$index", - description = "description: $index", - thumbnailUrl = "https://example.com/$index") - } } diff --git a/features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/features/seemore/ui/viewmodel/SeeMoreViewModelTest.kt b/features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/features/seemore/ui/viewmodel/SeeMoreViewModelTest.kt index b77bb3b..a346850 100644 --- a/features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/features/seemore/ui/viewmodel/SeeMoreViewModelTest.kt +++ b/features/seemore/ui/src/test/kotlin/com/manuelnunez/apps/features/seemore/ui/viewmodel/SeeMoreViewModelTest.kt @@ -1,22 +1,20 @@ package com.manuelnunez.apps.features.seemore.ui.viewmodel import androidx.paging.PagingData -import com.manuelnunez.apps.core.common.test.MockkAllRule -import com.manuelnunez.apps.core.common.test.UnMockkAllRule -import com.manuelnunez.apps.core.domain.model.Item +import com.manuelnunez.apps.core.common.testRule.MockkAllRule +import com.manuelnunez.apps.core.common.testRule.UnMockkAllRule +import com.manuelnunez.apps.core.domain.utils.mockItems import com.manuelnunez.apps.features.seemore.domain.usecase.GetAllItemUseCase import com.manuelnunez.apps.features.seemore.ui.SeeMoreViewModel import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flow import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import kotlin.properties.Delegates -@OptIn(ExperimentalCoroutinesApi::class) class SeeMoreViewModelTest { @RegisterExtension private val mockkAllExtension = MockkAllRule() @@ -38,6 +36,4 @@ class SeeMoreViewModelTest { verify(exactly = 1) { getAllItemUseCase.prepare(Unit) } confirmVerified(getAllItemUseCase) } - - private val mockItems = listOf(Item("1", "imageUrl1", "thumbnailUrl1", "description1")) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d89c41..817b6be 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,6 +28,9 @@ turbine = "1.0.0" pagingCommonKtx = "3.2.1" protobufPlugin = "0.9.4" protobuf = "3.25.2" +junitJunit = "4.13.2" +appcompat = "1.6.1" +material = "1.11.0" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" } @@ -42,6 +45,7 @@ androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui- androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } +androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHilt" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } @@ -55,6 +59,7 @@ kotlinx-serializer = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } +hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" } junit = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" } junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } @@ -71,6 +76,9 @@ androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" } +junit-junit = { group = "junit", name = "junit", version.ref = "junitJunit" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }