From 44978c69645a77c9682574812d37b91a6c6560b8 Mon Sep 17 00:00:00 2001 From: Santiago Mattiauda Date: Sun, 9 Feb 2025 14:56:45 -0300 Subject: [PATCH 1/2] [started] start activity tests --- app/build.gradle.kts | 4 ++-- .../template/ui/compose/HomeComposeViewModelDiTest.kt | 1 - .../core/data/client/network/RetrofitServiceCreator.kt | 8 ++------ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fd0a259..198f705 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -95,7 +95,7 @@ detekt { configurations { androidTestImplementation { - exclude( "io.mockk", "mockk-agent-jvm") + exclude("io.mockk", "mockk-agent-jvm") } } @@ -110,7 +110,7 @@ measureBuilds { } ksp { - arg("KOIN_CONFIG_CHECK","true") + arg("KOIN_CONFIG_CHECK", "true") } dependencies { diff --git a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt index cfcaddd..f6a9b28 100644 --- a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt +++ b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeViewModelDiTest.kt @@ -17,7 +17,6 @@ import com.santimattius.test.rules.MockWebServerRule import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith diff --git a/core/src/main/java/com/santimattius/core/data/client/network/RetrofitServiceCreator.kt b/core/src/main/java/com/santimattius/core/data/client/network/RetrofitServiceCreator.kt index b2829b7..4d1f1bd 100644 --- a/core/src/main/java/com/santimattius/core/data/client/network/RetrofitServiceCreator.kt +++ b/core/src/main/java/com/santimattius/core/data/client/network/RetrofitServiceCreator.kt @@ -4,20 +4,16 @@ import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -private fun createRetrofitService(baseUrl: String, apiKey: String): Retrofit { +class RetrofitServiceCreator(baseUrl: String, apiKey: String) { val client = OkHttpClient().newBuilder() .addInterceptor(RequestInterceptor(apiKey)) .build() - return Retrofit.Builder() + private val retrofit: Retrofit = Retrofit.Builder() .baseUrl(baseUrl) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() -} - -class RetrofitServiceCreator(baseUrl: String, apiKey: String) { - private val retrofit: Retrofit = createRetrofitService(baseUrl, apiKey) fun createService(serviceClass: Class): T { return retrofit.create(serviceClass) From 8e18d037493cb7486c65f6a2a044a249d191ef93 Mon Sep 17 00:00:00 2001 From: Santiago Mattiauda Date: Sun, 9 Feb 2025 18:08:55 -0300 Subject: [PATCH 2/2] [test] add activities tests --- app/build.gradle.kts | 1 + .../ui/compose/HomeComposeViewModel.kt | 7 +- .../ui/compose/ui/components/MovieItem.kt | 8 +- .../ui/compose/HomeComposeActivityDiTest.kt | 141 ++++++++++++++++++ .../ui/compose/HomeComposeActivityTest.kt | 29 +++- .../template/ui/rules/OkHttpComposeIdle.kt | 9 ++ .../template/ui/rules/RoomTestRule.kt | 5 +- .../test/data/FakeMovieLocalDataSource.kt | 22 ++- 8 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 app/src/test/java/com/santimattius/template/ui/compose/HomeComposeActivityDiTest.kt create mode 100644 app/src/test/java/com/santimattius/template/ui/rules/OkHttpComposeIdle.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 198f705..ffdfd69 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -168,6 +168,7 @@ dependencies { testImplementation(libs.bundles.robolectric) testImplementation(libs.junit) testImplementation(libs.turbine) + testImplementation(libs.okHttp3.idling.resource) //Android Testing debugImplementation(libs.androidx.fragment.testing.manifest) diff --git a/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeViewModel.kt b/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeViewModel.kt index 5658baa..31e5d14 100644 --- a/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeViewModel.kt +++ b/app/src/main/java/com/santimattius/template/ui/compose/HomeComposeViewModel.kt @@ -25,7 +25,12 @@ class HomeComposeViewModel( private val _state = MutableStateFlow(HomeUiState(isLoading = true)) val state: StateFlow = movieRepository.all.combine(_state) { movies, state -> - state.copy(movies = movies.asUiModels(), isLoading = false, loadingError = false) + val newMovies = movies.asUiModels() + if (newMovies != state.movies) { + state.copy(movies = newMovies, isLoading = false) + } else { + state // Si no ha cambiado nada, retorna el mismo estado + } }.onStart { movieRepository.refresh() }.catch { diff --git a/app/src/main/java/com/santimattius/template/ui/compose/ui/components/MovieItem.kt b/app/src/main/java/com/santimattius/template/ui/compose/ui/components/MovieItem.kt index 058c0f9..9514890 100644 --- a/app/src/main/java/com/santimattius/template/ui/compose/ui/components/MovieItem.kt +++ b/app/src/main/java/com/santimattius/template/ui/compose/ui/components/MovieItem.kt @@ -36,7 +36,7 @@ fun MovieCard( ) { Card( modifier = modifier - .testTag(item.title) + .testTag("card_${item.title}") .padding(dimensionResource(R.dimen.item_movie_padding)), ) { Box { @@ -59,11 +59,13 @@ fun MovieCard( ) IconButton( onClick = { onFavoriteClick(item) }, - modifier = Modifier.align(Alignment.BottomEnd) + modifier = Modifier + .align(Alignment.BottomEnd) + .testTag("favorite_${item.title}") ) { Icon( imageVector = if (item.favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, - contentDescription = null, + contentDescription = "Add ${item.title} to Favorite", tint = if (item.favorite) Color.Red else Color.White ) } diff --git a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeActivityDiTest.kt b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeActivityDiTest.kt new file mode 100644 index 0000000..cfef870 --- /dev/null +++ b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeActivityDiTest.kt @@ -0,0 +1,141 @@ +package com.santimattius.template.ui.compose + +import android.app.Application +import android.os.Build +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.test.core.app.ApplicationProvider +import com.jakewharton.espresso.OkHttp3IdlingResource +import com.santimattius.core.data.client.database.TheMovieDataBase +import com.santimattius.core.data.client.network.RetrofitServiceCreator +import com.santimattius.template.ui.rules.OkHttpComposeIdle +import com.santimattius.template.ui.rules.RoomTestRule +import com.santimattius.test.data.TheMovieDBServiceMother +import com.santimattius.test.rules.MainCoroutinesTestRule +import com.santimattius.test.rules.MockWebServerRule +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.koin.ksp.generated.com_santimattius_template_di_AppModule +import org.koin.ksp.generated.com_santimattius_template_di_DataModule +import org.koin.ksp.generated.defaultModule +import org.koin.test.KoinTest +import org.koin.test.KoinTestRule +import org.koin.test.get +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + + +@RunWith(RobolectricTestRunner::class) +@Config( + manifest = Config.NONE, + sdk = [Build.VERSION_CODES.R], + instrumentedPackages = ["androidx.loader.content"], + application = Application::class +) +class HomeComposeActivityDiTest : KoinTest { + + private lateinit var idle: OkHttpComposeIdle + private val appContext = ApplicationProvider.getApplicationContext() + + @get:Rule(order = 0) + val mockWebServerRule = MockWebServerRule { + TheMovieDBServiceMother.createPopularMovieResponse() + } + + @get:Rule(order = 1) + val roomTestRule = RoomTestRule(appContext, TheMovieDataBase::class.java) + + + @get:Rule(order = 2) + var koinTestRule = KoinTestRule.create { + modules( + com_santimattius_template_di_DataModule, + com_santimattius_template_di_AppModule, + defaultModule, + module { + //override base url and database + factory(named("base_url")) { mockWebServerRule.baseUrl } + factory { roomTestRule.db } + } + ) + } + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val coroutinesTestRule = MainCoroutinesTestRule() + + @OptIn(ExperimentalTestApi::class) + @get:Rule(order = 3) + val composeTestRule = createAndroidComposeRule( + HomeComposeActivity::class.java, + coroutinesTestRule.testDispatcher + ) + + @Before + fun setUp() { + val client = get().client + val idlingResource = OkHttp3IdlingResource.create("OkHttp", client) + idle = OkHttpComposeIdle(idlingResource) + + composeTestRule.registerIdlingResource(idle) + } + + @After + fun tearDown() { + composeTestRule.unregisterIdlingResource(idle) + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun `Verify first movie is Fantastic Beasts The Secrets of Dumbledore`() { + composeTestRule.waitForIdle() + composeTestRule.waitUntilDoesNotExist(hasTestTag("loading"), timeoutMillis = 10_000) + val title = "Fantastic Beasts: The Secrets of Dumbledore" + composeTestRule.waitUntilExactlyOneExists(hasTestTag("card_$title")) + composeTestRule.onNodeWithTag("card_$title").assertExists() + } + + + @OptIn(ExperimentalTestApi::class) + @Test + fun `onFavorite should update state with success message when adding to favorite succeeds`(){ + with(composeTestRule){ + + composeTestRule.waitForIdle() + composeTestRule.waitUntilDoesNotExist(hasTestTag("loading"), timeoutMillis = 10_000) + + val title = "Fantastic Beasts: The Secrets of Dumbledore" + composeTestRule.waitUntilExactlyOneExists(hasTestTag("card_$title")) + + onNodeWithTag("favorite_${title}") + .assertExists() + .performScrollTo() + .assertIsDisplayed() + .performClick() + + onNodeWithText("Add $title to favorite") + .assertIsDisplayed() + + onNodeWithText("OK").assertIsDisplayed().performClick() + + onNodeWithText("Add $title to favorite") + .assertDoesNotExist() + } + } + +} + diff --git a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeActivityTest.kt b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeActivityTest.kt index 410f38b..e28682d 100644 --- a/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeActivityTest.kt +++ b/app/src/test/java/com/santimattius/template/ui/compose/HomeComposeActivityTest.kt @@ -6,6 +6,9 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.runAndroidComposeUiTest import com.santimattius.test.rules.MainCoroutinesTestRule import org.junit.Rule @@ -43,13 +46,33 @@ class HomeComposeActivityTest { @get:Rule val coroutinesTestRule = MainCoroutinesTestRule() - @OptIn(ExperimentalTestApi::class) @Test fun `verify first movie is spider-man`() = runAndroidComposeUiTest(HomeComposeActivity::class.java) { - onNodeWithTag("Spider-Man: No Way Home") + onNodeWithTag("card_Spider-Man: No Way Home") + .assertIsDisplayed() + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun `onFavorite should update state with success message when adding to favorite succeeds`() = + runAndroidComposeUiTest(HomeComposeActivity::class.java) { + val title = "Spider-Man: No Way Home" + + onNodeWithTag("favorite_${title}") + .assertExists() + .performScrollTo() + .assertIsDisplayed() + .performClick() + + onNodeWithText("Add $title to favorite") .assertIsDisplayed() + + onNodeWithText("OK").assertIsDisplayed().performClick() + + onNodeWithText("Add $title to favorite") + .assertDoesNotExist() } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/santimattius/template/ui/rules/OkHttpComposeIdle.kt b/app/src/test/java/com/santimattius/template/ui/rules/OkHttpComposeIdle.kt new file mode 100644 index 0000000..63c2486 --- /dev/null +++ b/app/src/test/java/com/santimattius/template/ui/rules/OkHttpComposeIdle.kt @@ -0,0 +1,9 @@ +package com.santimattius.template.ui.rules + +import androidx.compose.ui.test.IdlingResource +import com.jakewharton.espresso.OkHttp3IdlingResource + +class OkHttpComposeIdle(private val idle: OkHttp3IdlingResource) : IdlingResource { + override val isIdleNow: Boolean + get() = idle.isIdleNow +} \ No newline at end of file diff --git a/app/src/test/java/com/santimattius/template/ui/rules/RoomTestRule.kt b/app/src/test/java/com/santimattius/template/ui/rules/RoomTestRule.kt index 7b8ca10..eeb097a 100644 --- a/app/src/test/java/com/santimattius/template/ui/rules/RoomTestRule.kt +++ b/app/src/test/java/com/santimattius/template/ui/rules/RoomTestRule.kt @@ -5,6 +5,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import org.junit.rules.TestWatcher import org.junit.runner.Description +import java.util.concurrent.Executors /** * A JUnit TestRule for testing Room database interactions. @@ -25,7 +26,9 @@ class RoomTestRule( override fun starting(description: Description?) { db = Room.inMemoryDatabaseBuilder(appContext, klass) - .allowMainThreadQueries().build() + .allowMainThreadQueries() +// .setTransactionExecutor(Executors.newSingleThreadExecutor()) + .build() } override fun finished(description: Description?) { diff --git a/shared-test/src/main/java/com/santimattius/test/data/FakeMovieLocalDataSource.kt b/shared-test/src/main/java/com/santimattius/test/data/FakeMovieLocalDataSource.kt index f90c786..8a72d96 100644 --- a/shared-test/src/main/java/com/santimattius/test/data/FakeMovieLocalDataSource.kt +++ b/shared-test/src/main/java/com/santimattius/test/data/FakeMovieLocalDataSource.kt @@ -78,11 +78,29 @@ class FakeMovieLocalDataSource : MovieLocalDataSource { } override suspend fun addToFavorite(movieId: Long): Result { - TODO("Not yet implemented") + return find(movieId).fold( + onSuccess = { + val movie = it.copy(favorite = true) + update(movie) + Result.success(Unit) + }, + onFailure = { + return Result.failure(it) + } + ) } override suspend fun removeFromFavorite(movieId: Long): Result { - TODO("Not yet implemented") + return find(movieId).fold( + onSuccess = { + val movie = it.copy(favorite = false) + update(movie) + Result.success(Unit) + }, + onFailure = { + return Result.failure(it) + } + ) } } \ No newline at end of file