diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b498ea6..5323801 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -75,12 +75,15 @@ dependencies { implementation(libs.coil.network) ksp(libs.hilt.android.compiler) testImplementation(libs.junit) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockk) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + testImplementation(kotlin("test")) } fun getSecret(key: String): String { diff --git a/app/src/test/java/xyz/leomurca/csgomatches/ExampleUnitTest.kt b/app/src/test/java/xyz/leomurca/csgomatches/ExampleUnitTest.kt deleted file mode 100644 index 30081c5..0000000 --- a/app/src/test/java/xyz/leomurca/csgomatches/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package xyz.leomurca.csgomatches - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/app/src/test/java/xyz/leomurca/csgomatches/data/remote/MatchRemoteDataSourceImplTest.kt b/app/src/test/java/xyz/leomurca/csgomatches/data/remote/MatchRemoteDataSourceImplTest.kt new file mode 100644 index 0000000..3fdbdb0 --- /dev/null +++ b/app/src/test/java/xyz/leomurca/csgomatches/data/remote/MatchRemoteDataSourceImplTest.kt @@ -0,0 +1,172 @@ +package xyz.leomurca.csgomatches.data.remote + +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.mockk +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.serialization.json.Json +import org.junit.After +import org.junit.Before +import org.junit.Test +import retrofit2.Response +import xyz.leomurca.csgomatches.data.model.ErrorDto +import xyz.leomurca.csgomatches.data.model.LeagueDto +import xyz.leomurca.csgomatches.data.model.MatchDto +import xyz.leomurca.csgomatches.data.model.SerieDto +import xyz.leomurca.csgomatches.data.model.TeamDetailsDto +import xyz.leomurca.csgomatches.domain.model.Resource +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) +class MatchRemoteDataSourceImplTest { + + @RelaxedMockK + private lateinit var api: MatchesApiService + + @RelaxedMockK + private lateinit var json: Json + + @InjectMockKs + private lateinit var dataSource: MatchRemoteDataSourceImpl + + @Before + fun setUp() { + MockKAnnotations.init(this) + Dispatchers.setMain(StandardTestDispatcher()) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `upcomingMatches - response is successful - return Resource Success with matches`() = + runTest { + // Arrange + val matches = listOf(MATCH_DTO) + coEvery { api.upcomingMatches() } returns Response.success(matches) + + // Act + val result = dataSource.upcomingMatches() + + // Assert + coVerify(exactly = 1) { + api.upcomingMatches("running, not_started", false, "begin_at") + } + assertTrue(result is Resource.Success) + assertEquals(matches, (result as Resource.Success).data) + } + + @Test + fun `upcomingMatches - response is an error - return api error with error body`() = runTest { + // Arrange + val errorDto = ErrorDto(message = "API failure") + + coEvery { api.upcomingMatches() } returns mockk(relaxed = true) { + every { isSuccessful } returns false + } + every { json.decodeFromString(any()) } returns errorDto + + // Act + val result = dataSource.upcomingMatches() + + // Assert + assertTrue(result is Resource.Error) + assertEquals("API failure", (result as Resource.Error).message) + } + + + @Test + fun `upcomingMatches - response is an RuntimeException - returns Resource Error`() = runTest { + // Arrange + coEvery { api.upcomingMatches() } throws RuntimeException("Network error") + + // Act + val result = dataSource.upcomingMatches() + + // Assert + assertTrue(result is Resource.Error) + assertEquals("Network error", (result as Resource.Error).message) + } + + @Test + fun `teamDetails - response is successful - returns Resource Success`() = runTest { + // Arrange + val teamDetails = listOf(TEAM_DETAILS_DTO) + coEvery { api.teamDetails("123") } returns Response.success(teamDetails) + + // Act + val result = dataSource.teamDetails("123") + + // Assert + assertTrue(result is Resource.Success) + assertEquals(teamDetails, (result as Resource.Success).data) + } + + + @Test + fun `teamDetails - response is an error - returns Resource Error`() = runTest { + // Arrange + val errorDto = ErrorDto(message = "Team not found") + + coEvery { api.teamDetails("456") } returns mockk(relaxed = true) { + every { isSuccessful } returns false + } + every { json.decodeFromString(any()) } returns errorDto + + // Act + val result = dataSource.teamDetails("456") + + // Assert + assertTrue(result is Resource.Error) + assertEquals("Team not found", (result as Resource.Error).message) + } + + @Test + fun `teamDetails - response is RuntimeException - returns Resource Error`() = runTest { + // Arrange + coEvery { api.teamDetails("789") } throws RuntimeException("Unknown error") + + // Act + val result = dataSource.teamDetails("789") + + // Assert + assertTrue(result is Resource.Error) + assertEquals("Unknown error", (result as Resource.Error).message) + } + + companion object { + private val MATCH_DTO = MatchDto( + beginAt = null, + opponents = emptyList(), + league = LeagueDto( + id = 5078, + name = "United21", + imageUrl = "https://cdn.pandascore.co/images/league/image/5078/800px-united21_allmode-png" + ), + serie = SerieDto( + id = 9519, + fullName = "Season 35 2025" + ), + status = "not_started" + ) + + private val TEAM_DETAILS_DTO = TeamDetailsDto( + id = 123L, + name = "Team 1", + imageUrl = "", + players = emptyList() + ) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d468cd..c220df1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,8 @@ kotlinxSerializationJson = "1.8.1" okhttp = "4.12.0" coil = "3.2.0" hiltNavigation = "1.2.0" +mockk = "1.14.5" +coroutinesTest = "1.10.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -45,6 +47,8 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } coil-network = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } +kotlinx-coroutines-test= { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutinesTest" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }