diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..e3d16bc --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,35 @@ +version: 2.1 + +orbs: + android: circleci/android@0.2.1 + +jobs: + build: + executor: android/android + + steps: + - checkout + + - run: + name: Chmod permissions for gradlew + command: sudo chmod +x ./gradlew + + - run: + name: Run ktlint + command: ./gradlew ktlintCheck + - store_artifacts: + path: build/reports + + - run: + name: Run detekt + command: ./gradlew detektAll + - store_artifacts: + path: build/staticAnalysis + + - run: + name: Run Domain Unit Tests + command: ./gradlew :domain:test + - store_artifacts: + path: domain/build/reports + - store_test_results: + path: domain/build/test-results \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 80f680f..07b2e75 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -5,6 +5,19 @@ * `$ ./gradlew buildSrcVersions` */ object Libs { + /** + * https://github.com/Kotlin/kotlinx.coroutines + */ + const val kotlinx_coroutines_android: String = + "org.jetbrains.kotlinx:kotlinx-coroutines-android:" + + Versions.org_jetbrains_kotlinx_kotlinx_coroutines + + /** + * https://github.com/Kotlin/kotlinx.coroutines + */ + const val kotlinx_coroutines_test: String = "org.jetbrains.kotlinx:kotlinx-coroutines-test:" + + Versions.org_jetbrains_kotlinx_kotlinx_coroutines + /** * https://kotlinlang.org/ */ @@ -71,8 +84,23 @@ object Libs { */ const val material: String = "com.google.android.material:material:" + Versions.material + /** + * https://github.com/pinterest/ktlint + */ + const val ktlint: String = "com.pinterest:ktlint:" + Versions.ktlint + /** * https://developer.android.com/studio */ const val aapt2: String = "com.android.tools.build:aapt2:" + Versions.aapt2 + + /** + * http://mockk.io + */ + const val mockk: String = "io.mockk:mockk:" + Versions.mockk + + /** + * http://github.com/google/truth + */ + const val truth: String = "com.google.truth:truth:" + Versions.truth } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index d5c4be3..3577702 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -11,6 +11,8 @@ import org.gradle.plugin.use.PluginDependencySpec * YOU are responsible for updating manually the dependency version. */ object Versions { + const val org_jetbrains_kotlinx_kotlinx_coroutines: String = "1.4.0" // available: "1.4.2" + const val org_jetbrains_kotlin: String = "1.4.21" const val com_android_tools_build_gradle: String = "4.1.1" @@ -31,8 +33,14 @@ object Versions { const val material: String = "1.2.1" + const val ktlint: String = "0.38.1" // available: "0.40.0" + const val aapt2: String = "4.1.1-6503028" + const val mockk: String = "1.10.3-jdk8" // available: "1.10.3" + + const val truth: String = "1.0.1" // available: "1.1" + /** * Current version: "6.5" * See issue 19: How to update Gradle itself? diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 36561dc..938da50 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -3,5 +3,9 @@ plugins { } dependencies { - + implementation(Libs.kotlinx_coroutines_android) + testImplementation(Libs.junit_junit) + testImplementation(Libs.mockk) + testImplementation(Libs.truth) + testImplementation(Libs.kotlinx_coroutines_test) } \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/Domain.kt b/domain/src/main/java/com/kryptkode/domain/Domain.kt deleted file mode 100644 index 7763dc6..0000000 --- a/domain/src/main/java/com/kryptkode/domain/Domain.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.kryptkode.domain - -class Domain { -} \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Character.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Character.kt new file mode 100644 index 0000000..f2bc13a --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Character.kt @@ -0,0 +1,8 @@ +package com.kryptkode.domain.charactersearch.entities + +data class Character( + val name: String, + val birthYear: String, + val height: String, + val url: String +) \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Film.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Film.kt new file mode 100644 index 0000000..f4854ae --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Film.kt @@ -0,0 +1,6 @@ +package com.kryptkode.domain.charactersearch.entities + +data class Film( + val title: String, + val openingCrawl: String +) \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Planet.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Planet.kt new file mode 100644 index 0000000..4c22c52 --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Planet.kt @@ -0,0 +1,10 @@ +package com.kryptkode.domain.charactersearch.entities + +data class Planet( + val name: String, + val population: String +) { + companion object { + val NO_PLANET = Planet("", "") + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Specie.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Specie.kt new file mode 100644 index 0000000..5da2ee2 --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/entities/Specie.kt @@ -0,0 +1,7 @@ +package com.kryptkode.domain.charactersearch.entities + +data class Specie( + val name: String, + val language: String, + val homeWorld: String +) diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/repo/CharacterDetailRepository.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/repo/CharacterDetailRepository.kt new file mode 100644 index 0000000..4b3a638 --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/repo/CharacterDetailRepository.kt @@ -0,0 +1,14 @@ +package com.kryptkode.domain.charactersearch.repo + +import com.kryptkode.domain.charactersearch.entities.Film +import com.kryptkode.domain.charactersearch.entities.Planet +import com.kryptkode.domain.charactersearch.entities.Specie +import kotlinx.coroutines.flow.Flow + +interface CharacterDetailRepository { + fun fetchPlanet(planetUrl: String): Flow + + fun fetchSpecies(urls: List): Flow> + + fun fetchFilms(urls: List): Flow> +} \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/repo/SearchCharactersRepository.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/repo/SearchCharactersRepository.kt new file mode 100644 index 0000000..b931fcd --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/repo/SearchCharactersRepository.kt @@ -0,0 +1,8 @@ +package com.kryptkode.domain.charactersearch.repo + +import com.kryptkode.domain.charactersearch.entities.Character +import kotlinx.coroutines.flow.Flow + +interface SearchCharactersRepository { + fun searchCharacters(characterName: String): Flow> +} \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/FetchFilmsUseCase.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/FetchFilmsUseCase.kt new file mode 100644 index 0000000..f98b963 --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/FetchFilmsUseCase.kt @@ -0,0 +1,20 @@ +package com.kryptkode.domain.charactersearch.usecases + +import com.kryptkode.domain.charactersearch.entities.Film +import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository +import com.kryptkode.domain.dispatchers.AppDispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn + +class FetchFilmsUseCase( + private val appDispatchers: AppDispatchers, + private val repository: CharacterDetailRepository) { + + fun fetchFilms(urls: List): Flow> { + if (urls.isEmpty()) { + return flowOf(emptyList()) + } + return repository.fetchFilms(urls).flowOn(appDispatchers.io) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/FetchPlanetUseCase.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/FetchPlanetUseCase.kt new file mode 100644 index 0000000..34ee897 --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/FetchPlanetUseCase.kt @@ -0,0 +1,22 @@ +package com.kryptkode.domain.charactersearch.usecases + +import com.kryptkode.domain.charactersearch.entities.Planet +import com.kryptkode.domain.charactersearch.entities.Planet.Companion.NO_PLANET +import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository +import com.kryptkode.domain.dispatchers.AppDispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn + +class FetchPlanetUseCase(private val dispatchers: AppDispatchers, + private val repository: CharacterDetailRepository) { + + fun fetchPlanet(planetUrl: String): Flow { + if(planetUrl.isEmpty()){ + return flowOf(NO_PLANET) + } + return repository.fetchPlanet(planetUrl).flowOn(dispatchers.io) + } + + +} \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/FetchSpeciesUseCase.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/FetchSpeciesUseCase.kt new file mode 100644 index 0000000..b67cfc5 --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/FetchSpeciesUseCase.kt @@ -0,0 +1,22 @@ +package com.kryptkode.domain.charactersearch.usecases + +import com.kryptkode.domain.charactersearch.entities.Specie +import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository +import com.kryptkode.domain.dispatchers.AppDispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn + +class FetchSpeciesUseCase( + private val dispatchers: AppDispatchers, + private val repository: CharacterDetailRepository +) { + fun fetchSpecies(urls: List): Flow> { + if (urls.isEmpty()) { + return flowOf(emptyList()) + } + return repository.fetchSpecies(urls).flowOn(dispatchers.io) + } + + +} \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/SearchCharactersUseCase.kt b/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/SearchCharactersUseCase.kt new file mode 100644 index 0000000..33e0a41 --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/charactersearch/usecases/SearchCharactersUseCase.kt @@ -0,0 +1,16 @@ +package com.kryptkode.domain.charactersearch.usecases + +import com.kryptkode.domain.charactersearch.entities.Character +import com.kryptkode.domain.charactersearch.repo.SearchCharactersRepository +import com.kryptkode.domain.dispatchers.AppDispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +class SearchCharactersUseCase( + private val appDispatchers: AppDispatchers, + private val searchCharactersRepository: SearchCharactersRepository +) { + fun searchCharacters(query: String): Flow> { + return searchCharactersRepository.searchCharacters(query).flowOn(appDispatchers.io) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/kryptkode/domain/dispatchers/AppDispatchers.kt b/domain/src/main/java/com/kryptkode/domain/dispatchers/AppDispatchers.kt new file mode 100644 index 0000000..e82e1fa --- /dev/null +++ b/domain/src/main/java/com/kryptkode/domain/dispatchers/AppDispatchers.kt @@ -0,0 +1,9 @@ +package com.kryptkode.domain.dispatchers + +import kotlinx.coroutines.CoroutineDispatcher + +interface AppDispatchers { + val main: CoroutineDispatcher + val io: CoroutineDispatcher + val default: CoroutineDispatcher +} \ No newline at end of file diff --git a/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/FetchFilmsUseCaseTest.kt b/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/FetchFilmsUseCaseTest.kt new file mode 100644 index 0000000..4154388 --- /dev/null +++ b/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/FetchFilmsUseCaseTest.kt @@ -0,0 +1,101 @@ +package com.kryptkode.domain.charactersearch.usecases + +import com.google.common.truth.Truth.assertThat +import com.kryptkode.domain.charactersearch.entities.Film +import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository +import com.kryptkode.domain.dispatchers.AppDispatchers +import com.kryptkode.domain.utils.DataFactory.randomString +import com.kryptkode.domain.utils.MockDataFactory.makeFilm +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import org.junit.After +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class FetchFilmsUseCaseTest { + + private lateinit var dispatchers: AppDispatchers + private lateinit var repository: CharacterDetailRepository + private lateinit var SUT: FetchFilmsUseCase + + @Before + fun setUp() { + dispatchers = mockk() + repository = mockk() + SUT = FetchFilmsUseCase(dispatchers, repository) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `fetching films calls repository with passed parameter`() { + stubDispatchers() + stubRepo() + val testQuery = listOf(randomString(), randomString()) + SUT.fetchFilms(testQuery) + verify(exactly = 1) { + repository.fetchFilms(testQuery) + } + } + + @Test + fun `fetching films returns data`() = runBlocking { + stubDispatchers() + val testCharacters = listOf(makeFilm(), makeFilm()) + stubRepo(testCharacters) + + val testQuery = listOf(randomString(), randomString()) + val result = SUT.fetchFilms(testQuery).first() + + assertThat(result).isEqualTo(testCharacters) + } + + @Test + fun `fetching films with empty urls returns empty films list`() = runBlocking { + val result = SUT.fetchFilms(emptyList()) + assertThat(result.first()).isEmpty() + } + + @Test + fun `fetching films runs on io dispatcher`() { + stubDispatchers() + stubRepo() + + val testUrls = listOf(randomString(), randomString()) + SUT.fetchFilms(testUrls) + verify(exactly = 1) { + dispatchers.io + } + } + + private fun stubDispatchers() { + every { + dispatchers.default + } returns TestCoroutineDispatcher() + + every { + dispatchers.io + } returns TestCoroutineDispatcher() + + every { + dispatchers.main + } returns TestCoroutineDispatcher() + } + + private fun stubRepo(list: List = emptyList()) { + every { + repository.fetchFilms(any()) + } returns flowOf(list) + } +} \ No newline at end of file diff --git a/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/FetchPlanetUseCaseTest.kt b/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/FetchPlanetUseCaseTest.kt new file mode 100644 index 0000000..a7482ae --- /dev/null +++ b/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/FetchPlanetUseCaseTest.kt @@ -0,0 +1,102 @@ +package com.kryptkode.domain.charactersearch.usecases + +import com.google.common.truth.Truth.assertThat +import com.kryptkode.domain.charactersearch.entities.Planet +import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository +import com.kryptkode.domain.dispatchers.AppDispatchers +import com.kryptkode.domain.utils.DataFactory.randomString +import com.kryptkode.domain.utils.MockDataFactory.makePlanet +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import org.junit.After +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class FetchPlanetUseCaseTest { + private lateinit var dispatchers: AppDispatchers + private lateinit var repository: CharacterDetailRepository + private lateinit var SUT: FetchPlanetUseCase + + @Before + fun setUp() { + dispatchers = mockk() + repository = mockk() + SUT = FetchPlanetUseCase(dispatchers, repository) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `fetching planet calls repository with passed parameter`() { + stubDispatchers() + stubRepo() + val testUrl = randomString() + + SUT.fetchPlanet(testUrl) + + verify(exactly = 1) { + repository.fetchPlanet(testUrl) + } + } + + @Test + fun `fetching planet returns data`() = runBlocking { + stubDispatchers() + val testPlanet = makePlanet() + stubRepo(testPlanet) + + val testUrl = randomString() + val result = SUT.fetchPlanet(testUrl).first() + + assertThat(result).isEqualTo(testPlanet) + } + + @Test + fun `fetching planet with empty string returns empty planet`() = runBlocking { + val result = SUT.fetchPlanet("") + assertThat(result.first()).isEqualTo(Planet.NO_PLANET) + } + + @Test + fun `fetching planet runs on io dispatcher`() { + stubDispatchers() + stubRepo() + + val testUrl = randomString() + SUT.fetchPlanet(testUrl) + verify(exactly = 1) { + dispatchers.io + } + } + + private fun stubDispatchers() { + every { + dispatchers.default + } returns TestCoroutineDispatcher() + + every { + dispatchers.io + } returns TestCoroutineDispatcher() + + every { + dispatchers.main + } returns TestCoroutineDispatcher() + } + + private fun stubRepo(planet: Planet = Planet.NO_PLANET) { + every { + repository.fetchPlanet(any()) + } returns flowOf(planet) + } +} \ No newline at end of file diff --git a/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/FetchSpeciesUseCaseTest.kt b/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/FetchSpeciesUseCaseTest.kt new file mode 100644 index 0000000..0ef511f --- /dev/null +++ b/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/FetchSpeciesUseCaseTest.kt @@ -0,0 +1,102 @@ +package com.kryptkode.domain.charactersearch.usecases + +import com.google.common.truth.Truth +import com.kryptkode.domain.charactersearch.entities.Specie +import com.kryptkode.domain.charactersearch.repo.CharacterDetailRepository +import com.kryptkode.domain.dispatchers.AppDispatchers +import com.kryptkode.domain.utils.DataFactory.randomString +import com.kryptkode.domain.utils.MockDataFactory.makeSpecie +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import org.junit.After +import org.junit.Before +import org.junit.Test + + +@ExperimentalCoroutinesApi +class FetchSpeciesUseCaseTest { + + private lateinit var dispatchers: AppDispatchers + private lateinit var repository: CharacterDetailRepository + private lateinit var SUT: FetchSpeciesUseCase + + @Before + fun setUp() { + dispatchers = mockk() + repository = mockk() + SUT = FetchSpeciesUseCase(dispatchers, repository) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `fetching species calls repository with passed parameter`() { + stubDispatchers() + stubRepo() + val testUrls = listOf(randomString(), randomString()) + SUT.fetchSpecies(testUrls) + verify(exactly = 1) { + repository.fetchSpecies(testUrls) + } + } + + @Test + fun `fetching species returns data`() = runBlocking { + stubDispatchers() + val testCharacters = listOf(makeSpecie(), makeSpecie()) + stubRepo(testCharacters) + + val testQuery = listOf(randomString(), randomString()) + val result = SUT.fetchSpecies(testQuery).first() + + Truth.assertThat(result).isEqualTo(testCharacters) + } + + @Test + fun `fetching species with empty urls returns empty species list`() = runBlocking { + val result = SUT.fetchSpecies(emptyList()) + Truth.assertThat(result.first()).isEmpty() + } + + @Test + fun `fetching species runs on io dispatcher`() { + stubDispatchers() + stubRepo() + + val testUrls = listOf(randomString(), randomString()) + SUT.fetchSpecies(testUrls) + verify(exactly = 1) { + dispatchers.io + } + } + + private fun stubDispatchers() { + every { + dispatchers.default + } returns TestCoroutineDispatcher() + + every { + dispatchers.io + } returns TestCoroutineDispatcher() + + every { + dispatchers.main + } returns TestCoroutineDispatcher() + } + + private fun stubRepo(list: List = emptyList()) { + every { + repository.fetchSpecies(any()) + } returns flowOf(list) + } +} \ No newline at end of file diff --git a/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/SearchCharactersUseCaseTest.kt b/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/SearchCharactersUseCaseTest.kt new file mode 100644 index 0000000..37448a9 --- /dev/null +++ b/domain/src/test/java/com/kryptkode/domain/charactersearch/usecases/SearchCharactersUseCaseTest.kt @@ -0,0 +1,95 @@ +package com.kryptkode.domain.charactersearch.usecases + +import com.google.common.truth.Truth.assertThat +import com.kryptkode.domain.charactersearch.entities.Character +import com.kryptkode.domain.charactersearch.repo.SearchCharactersRepository +import com.kryptkode.domain.dispatchers.AppDispatchers +import com.kryptkode.domain.utils.DataFactory.randomString +import com.kryptkode.domain.utils.MockDataFactory.makeCharacter +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import org.junit.After +import org.junit.Before +import org.junit.Test + +@ExperimentalCoroutinesApi +class SearchCharactersUseCaseTest { + + private lateinit var dispatchers: AppDispatchers + private lateinit var repository: SearchCharactersRepository + private lateinit var SUT: SearchCharactersUseCase + + @Before + fun setup() { + dispatchers = mockk() + repository = mockk() + SUT = SearchCharactersUseCase(dispatchers, repository) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `searching characters calls repository with passed query`() { + stubDispatchers() + stubRepo() + val testQuery = randomString() + SUT.searchCharacters(testQuery) + verify(exactly = 1) { + repository.searchCharacters(testQuery) + } + } + + @Test + fun `searching characters returns data`() = runBlocking { + stubDispatchers() + val testCharacters = listOf(makeCharacter(), makeCharacter()) + stubRepo(testCharacters) + + val testQuery = randomString() + val result = SUT.searchCharacters(testQuery).first() + + assertThat(result).isEqualTo(testCharacters) + } + + @Test + fun `searching characters runs on io dispatcher`() { + stubDispatchers() + stubRepo() + + val testQuery = randomString() + SUT.searchCharacters(testQuery) + verify(exactly = 1) { + dispatchers.io + } + } + + private fun stubDispatchers() { + every { + dispatchers.default + } returns TestCoroutineDispatcher() + + every { + dispatchers.io + } returns TestCoroutineDispatcher() + + every { + dispatchers.main + } returns TestCoroutineDispatcher() + } + + private fun stubRepo(list: List = emptyList()) { + every { + repository.searchCharacters(any()) + } returns flowOf(list) + } +} \ No newline at end of file diff --git a/domain/src/test/java/com/kryptkode/domain/utils/DataFactory.kt b/domain/src/test/java/com/kryptkode/domain/utils/DataFactory.kt new file mode 100644 index 0000000..07fb868 --- /dev/null +++ b/domain/src/test/java/com/kryptkode/domain/utils/DataFactory.kt @@ -0,0 +1,23 @@ +package com.kryptkode.domain.utils + +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +object DataFactory { + + fun randomString(): String { + return UUID.randomUUID().toString() + } + + fun randomInt(): Int { + return ThreadLocalRandom.current().nextInt(0, 1000 + 1) + } + + fun randomLong(): Long { + return randomInt().toLong() + } + + fun randomBoolean(): Boolean { + return Math.random() < 0.5 + } +} \ No newline at end of file diff --git a/domain/src/test/java/com/kryptkode/domain/utils/MockDataFactory.kt b/domain/src/test/java/com/kryptkode/domain/utils/MockDataFactory.kt new file mode 100644 index 0000000..58626f3 --- /dev/null +++ b/domain/src/test/java/com/kryptkode/domain/utils/MockDataFactory.kt @@ -0,0 +1,41 @@ +package com.kryptkode.domain.utils + +import com.kryptkode.domain.charactersearch.entities.Character +import com.kryptkode.domain.charactersearch.entities.Film +import com.kryptkode.domain.charactersearch.entities.Planet +import com.kryptkode.domain.charactersearch.entities.Specie +import com.kryptkode.domain.utils.DataFactory.randomString + +object MockDataFactory { + + fun makeCharacter(): Character { + return Character( + randomString(), + randomString(), + randomString(), + randomString(), + ) + } + + fun makeFilm(): Film { + return Film( + randomString(), + randomString(), + ) + } + + fun makePlanet(): Planet { + return Planet( + randomString(), + randomString(), + ) + } + + fun makeSpecie(): Specie { + return Specie( + randomString(), + randomString(), + randomString(), + ) + } +} \ No newline at end of file diff --git a/domain/src/test/java/com/kryptkode/domain/utils/TestAppDispatchers.kt b/domain/src/test/java/com/kryptkode/domain/utils/TestAppDispatchers.kt new file mode 100644 index 0000000..e94cbba --- /dev/null +++ b/domain/src/test/java/com/kryptkode/domain/utils/TestAppDispatchers.kt @@ -0,0 +1,13 @@ +package com.kryptkode.domain.utils + +import com.kryptkode.domain.dispatchers.AppDispatchers +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher + +@ExperimentalCoroutinesApi +class TestAppDispatchers : AppDispatchers { + override val default: CoroutineDispatcher = TestCoroutineDispatcher() + override val io: CoroutineDispatcher = TestCoroutineDispatcher() + override val main: CoroutineDispatcher = TestCoroutineDispatcher() +} \ No newline at end of file