Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ detekt {

configurations {
androidTestImplementation {
exclude( "io.mockk", "mockk-agent-jvm")
exclude("io.mockk", "mockk-agent-jvm")
}
}

Expand All @@ -110,7 +110,7 @@ measureBuilds {
}

ksp {
arg("KOIN_CONFIG_CHECK","true")
arg("KOIN_CONFIG_CHECK", "true")
}

dependencies {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ class HomeComposeViewModel(

private val _state = MutableStateFlow(HomeUiState(isLoading = true))
val state: StateFlow<HomeUiState> = 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fun MovieCard(
) {
Card(
modifier = modifier
.testTag(item.title)
.testTag("card_${item.title}")
.padding(dimensionResource(R.dimen.item_movie_padding)),
) {
Box {
Expand All @@ -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
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Application>()

@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<TheMovieDataBase> { 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<RetrofitServiceCreator>().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()
}
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -25,7 +26,9 @@ class RoomTestRule<T : RoomDatabase>(

override fun starting(description: Description?) {
db = Room.inMemoryDatabaseBuilder(appContext, klass)
.allowMainThreadQueries().build()
.allowMainThreadQueries()
// .setTransactionExecutor(Executors.newSingleThreadExecutor())
.build()
}

override fun finished(description: Description?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> createService(serviceClass: Class<T>): T {
return retrofit.create(serviceClass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,29 @@ class FakeMovieLocalDataSource : MovieLocalDataSource {
}

override suspend fun addToFavorite(movieId: Long): Result<Unit> {
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<Unit> {
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)
}
)
}

}