diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0bb98174..856c2d43 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,25 +10,25 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
-
- - name: Set up JDK
- uses: actions/setup-java@v2
- with:
- distribution: 'zulu'
- java-version: '11'
-
- - name: Make gradlew executable
- run: chmod +x ./gradlew
-
- - name: Spotless check
- run: ./gradlew spotlessCheck
-
- - name: Build debug APK
- run: ./gradlew assembleDebug --warning-mode all --stacktrace
-
- - name: Upload APK
- uses: actions/upload-artifact@v2
- with:
- name: app-debug
- path: app/build/outputs/apk/debug/app-debug.apk
+ - uses: actions/checkout@v2
+
+ - name: Set up JDKd
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'zulu'
+ java-version: '11'
+
+ - name: Make gradlew executable
+ run: chmod +x ./gradlew
+
+ - name: Spotless check
+ run: ./gradlew spotlessCheck
+
+ - name: Build debug APK
+ run: ./gradlew assembleDebug --warning-mode all --stacktrace
+
+ - name: Upload APK
+ uses: actions/upload-artifact@v2
+ with:
+ name: app-debug
+ path: app/build/outputs/apk/debug/app-debug.apk
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
new file mode 100644
index 00000000..df9e4c0d
--- /dev/null
+++ b/.github/workflows/unit-test.yml
@@ -0,0 +1,31 @@
+name: Unit Tests CI
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Set up JDK
+ uses: actions/setup-java@v2
+ with:
+ distribution: 'zulu'
+ java-version: '11'
+
+ - name: Make gradlew executable
+ run: chmod +x ./gradlew
+
+ - name: Run Android Debug Unit Test
+ run: ./gradlew jacocoTestReportDebug --warning-mode all --stacktrace
+
+ - name: Run Java/Kotlin Unit Test
+ run: ./gradlew jacocoTestReport --warning-mode all --stacktrace
+
+ - name: Upload Test Report
+ uses: codecov/codecov-action@v2.1.0
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 7e7ee626..fb7f4a8a 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 076b3e47..3934ffb9 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 822eb9d1..19e762b8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
androidApplication
kotlinAndroid
+ jacoco
}
android {
@@ -26,6 +27,10 @@ android {
"proguard-rules.pro"
)
}
+
+// getByName("debug") {
+// isTestCoverageEnabled = true
+// }
}
compileOptions {
@@ -34,6 +39,11 @@ android {
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() }
buildFeatures { viewBinding = true }
+
+ testOptions {
+ unitTests.isIncludeAndroidResources = true
+ unitTests.isReturnDefaultValues = true
+ }
}
dependencies {
@@ -61,4 +71,6 @@ dependencies {
testImplementation(deps.test.junit)
androidTestImplementation(deps.test.androidxJunit)
androidTestImplementation(deps.test.androidXSspresso)
+
+ addUnitTest()
}
diff --git a/app/src/test/java/com/hoc/flowmvi/ExampleUnitTest.kt b/app/src/test/java/com/hoc/flowmvi/ExampleUnitTest.kt
index 6e1e6f17..018f2743 100644
--- a/app/src/test/java/com/hoc/flowmvi/ExampleUnitTest.kt
+++ b/app/src/test/java/com/hoc/flowmvi/ExampleUnitTest.kt
@@ -1,7 +1,7 @@
package com.hoc.flowmvi
-import org.junit.Assert.assertEquals
-import org.junit.Test
+import kotlin.test.Test
+import kotlin.test.assertEquals
/**
* Example local unit test, which will execute on the development machine (host).
diff --git a/build.gradle.kts b/build.gradle.kts
index 0903c0f4..0c383f32 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,17 +7,21 @@ buildscript {
google()
mavenCentral()
gradlePluginPortal()
+ maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
}
dependencies {
classpath("com.android.tools.build:gradle:7.0.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
classpath("com.diffplug.spotless:spotless-plugin-gradle:5.15.1")
classpath("dev.ahmedmourad.nocopy:nocopy-gradle-plugin:1.4.0")
+ classpath("org.jacoco:org.jacoco.core:0.8.7")
+ classpath("com.vanniktech:gradle-android-junit-jacoco-plugin:0.17.0-SNAPSHOT")
}
}
subprojects {
apply(plugin = "com.diffplug.spotless")
+ apply(plugin = "com.vanniktech.android.junit.jacoco")
configure {
kotlin {
@@ -59,6 +63,26 @@ subprojects {
endWithNewline()
}
}
+
+ configure {
+ jacocoVersion = "0.8.7"
+ includeNoLocationClasses = true
+ includeInstrumentationCoverageInMergedReport = true
+ csv.isEnabled = false
+ xml.isEnabled = true
+ html.isEnabled = true
+ }
+
+ afterEvaluate {
+ tasks.withType {
+ extensions
+ .getByType()
+ .run {
+ isIncludeNoLocationClasses = true
+ excludes = listOf("jdk.internal.*")
+ }
+ }
+ }
}
allprojects {
diff --git a/buildSrc/src/main/kotlin/deps.kt b/buildSrc/src/main/kotlin/deps.kt
index 96b9d0f2..9eae5c6e 100644
--- a/buildSrc/src/main/kotlin/deps.kt
+++ b/buildSrc/src/main/kotlin/deps.kt
@@ -1,6 +1,7 @@
@file:Suppress("unused", "ClassName", "SpellCheckingInspection")
import org.gradle.api.artifacts.dsl.DependencyHandler
+import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.project
import org.gradle.plugin.use.PluginDependenciesSpec
import org.gradle.plugin.use.PluginDependencySpec
@@ -11,8 +12,8 @@ const val kotlinVersion = "1.5.21"
object appConfig {
const val applicationId = "com.hoc.flowmvi"
- const val compileSdkVersion = 30
- const val buildToolsVersion = "30.0.3"
+ const val compileSdkVersion = 31
+ const val buildToolsVersion = "31.0.0"
const val minSdkVersion = 21
const val targetSdkVersion = 30
@@ -51,6 +52,7 @@ object deps {
const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"
+ const val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$version"
}
object koin {
@@ -65,9 +67,12 @@ object deps {
const val flowExt = "io.github.hoc081098:FlowExt:0.0.7-SNAPSHOT"
object test {
- const val junit = "junit:junit:4.13"
+ const val junit = "junit:junit:4.13.2"
const val androidxJunit = "androidx.test.ext:junit:1.1.2"
const val androidXSspresso = "androidx.test.espresso:espresso-core:3.3.0"
+
+ const val mockk = "io.mockk:mockk:1.12.0"
+ const val kotlinJUnit = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion"
}
}
@@ -85,3 +90,10 @@ inline val DependencyHandler.data get() = project(":data")
inline val DependencyHandler.featureMain get() = project(":feature-main")
inline val DependencyHandler.featureAdd get() = project(":feature-add")
inline val DependencyHandler.featureSearch get() = project(":feature-search")
+
+fun DependencyHandler.addUnitTest() {
+ add("testImplementation", deps.test.junit)
+ add("testImplementation", deps.test.mockk)
+ add("testImplementation", deps.test.kotlinJUnit)
+ add("testImplementation", deps.coroutines.test)
+}
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index f775595d..d15aa08f 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -29,6 +29,11 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() }
+
+ testOptions {
+ unitTests.isIncludeAndroidResources = true
+ unitTests.isReturnDefaultValues = true
+ }
}
dependencies {
@@ -42,4 +47,6 @@ dependencies {
implementation(deps.lifecycle.commonJava8)
implementation(deps.lifecycle.runtimeKtx)
+
+ addUnitTest()
}
diff --git a/core/src/test/java/com/hoc/flowmvi/core/ExampleUnitTest.kt b/core/src/test/java/com/hoc/flowmvi/core/ExampleUnitTest.kt
index dc6f3377..a730e1aa 100644
--- a/core/src/test/java/com/hoc/flowmvi/core/ExampleUnitTest.kt
+++ b/core/src/test/java/com/hoc/flowmvi/core/ExampleUnitTest.kt
@@ -1 +1,11 @@
package com.hoc.flowmvi.core
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/coverage.gradle.kts b/coverage.gradle.kts
new file mode 100644
index 00000000..aa6b4a14
--- /dev/null
+++ b/coverage.gradle.kts
@@ -0,0 +1,36 @@
+apply(plugin = "jacoco")
+
+tasks {
+ val debugCoverageReport by registering(JacocoReport::class)
+ debugCoverageReport {
+ dependsOn("testDebugUnitTest")
+
+ reports {
+ xml.run {
+ required.value(true)
+ outputLocation.set(file("$buildDir/reports/jacoco/test/jacocoTestReport.xml"))
+ }
+ html.required.value(true)
+ }
+
+ val kotlinClasses = fileTree("$buildDir/tmp/kotlin-classes/debug")
+ val coverageSourceDirs = arrayOf(
+ "src/main/java",
+ "src/debug/java"
+ )
+ val executionDataDirs = fileTree("$buildDir") {
+ setIncludes(
+ listOf(
+ "jacoco/testDebugUnitTest.exec",
+ "outputs/code_coverage/debugAndroidTest/connected/*.ec",
+ "outputs/code-coverage/connected/*coverage.ec"
+ )
+ )
+ }
+
+ classDirectories.setFrom(files(kotlinClasses))
+ sourceDirectories.setFrom(coverageSourceDirs)
+ additionalSourceDirs.setFrom(files(coverageSourceDirs))
+ executionData.setFrom(executionDataDirs)
+ }
+}
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index 0c8c9167..ea7c8251 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -29,6 +29,11 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() }
+
+ testOptions {
+ unitTests.isIncludeAndroidResources = true
+ unitTests.isReturnDefaultValues = true
+ }
}
dependencies {
@@ -43,4 +48,6 @@ dependencies {
implementation(deps.squareup.loggingInterceptor)
implementation(deps.koin.core)
+
+ addUnitTest()
}
diff --git a/data/src/test/java/com/hoc/flowmvi/data/ExampleUnitTest.kt b/data/src/test/java/com/hoc/flowmvi/data/ExampleUnitTest.kt
index f160a149..85c2e904 100644
--- a/data/src/test/java/com/hoc/flowmvi/data/ExampleUnitTest.kt
+++ b/data/src/test/java/com/hoc/flowmvi/data/ExampleUnitTest.kt
@@ -1,12 +1,8 @@
package com.hoc.flowmvi.data
-import org.junit.Test
+import kotlin.test.Test
+import kotlin.test.assertEquals
-/**
- * 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() {
diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts
index 45c82b6d..70551289 100644
--- a/domain/build.gradle.kts
+++ b/domain/build.gradle.kts
@@ -5,4 +5,6 @@ plugins {
dependencies {
implementation(deps.coroutines.core)
implementation(deps.koin.core)
+
+ addUnitTest()
}
diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/ExampleTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/ExampleTest.kt
new file mode 100644
index 00000000..7ae3f4f7
--- /dev/null
+++ b/domain/src/test/java/com/hoc/flowmvi/domain/ExampleTest.kt
@@ -0,0 +1,11 @@
+package com.hoc.flowmvi.domain
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class ExampleTest {
+ @Test
+ fun example() {
+ assertEquals(1, 1)
+ }
+}
diff --git a/feature-add/build.gradle.kts b/feature-add/build.gradle.kts
index 0a59bb08..af74ddcc 100644
--- a/feature-add/build.gradle.kts
+++ b/feature-add/build.gradle.kts
@@ -29,8 +29,12 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() }
-
buildFeatures { viewBinding = true }
+
+ testOptions {
+ unitTests.isIncludeAndroidResources = true
+ unitTests.isReturnDefaultValues = true
+ }
}
dependencies {
@@ -51,4 +55,6 @@ dependencies {
implementation(deps.viewBindingDelegate)
implementation(deps.flowExt)
+
+ addUnitTest()
}
diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/ExampleUnitTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/ExampleUnitTest.kt
index 8b9356a7..388cfb42 100644
--- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/ExampleUnitTest.kt
+++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/ExampleUnitTest.kt
@@ -1,12 +1,8 @@
package com.hoc.flowmvi.ui.add
-import org.junit.Test
+import kotlin.test.Test
+import kotlin.test.assertEquals
-/**
- * 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() {
diff --git a/feature-main/build.gradle.kts b/feature-main/build.gradle.kts
index 83ea61ed..a47fb278 100644
--- a/feature-main/build.gradle.kts
+++ b/feature-main/build.gradle.kts
@@ -22,6 +22,10 @@ android {
"proguard-rules.pro"
)
}
+
+// getByName("debug") {
+// isTestCoverageEnabled = true
+// }
}
compileOptions {
@@ -31,6 +35,13 @@ android {
kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() }
buildFeatures { viewBinding = true }
+
+ testOptions {
+ unitTests {
+ isReturnDefaultValues = true
+ isIncludeAndroidResources = true
+ }
+ }
}
dependencies {
@@ -53,4 +64,6 @@ dependencies {
implementation(deps.coil)
implementation(deps.viewBindingDelegate)
implementation(deps.flowExt)
+
+ addUnitTest()
}
diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
index 09599d55..b8d092f4 100644
--- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
+++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNot
@@ -85,49 +86,52 @@ internal class MainVM(
}
}
- private fun Flow.toPartialChangeFlow(): Flow = shareIn(viewModelScope, SharingStarted.WhileSubscribed()).run {
- val getUserChanges = getUsersUseCase()
- .onEach { Log.d("###", "[MAIN_VM] Emit users.size=${it.size}") }
- .map {
- val items = it.map(::UserItem)
- PartialChange.GetUser.Data(items) as PartialChange.GetUser
- }
- .onStart { emit(PartialChange.GetUser.Loading) }
- .catch { emit(PartialChange.GetUser.Error(it)) }
+ private fun Flow.toPartialChangeFlow(): Flow =
+ shareIn(viewModelScope, SharingStarted.WhileSubscribed()).run {
+ val getUserChanges = defer(getUsersUseCase::invoke)
+ .onEach { Log.d("###", "[MAIN_VM] Emit users.size=${it.size}") }
+ .map {
+ val items = it.map(::UserItem)
+ PartialChange.GetUser.Data(items) as PartialChange.GetUser
+ }
+ .onStart { emit(PartialChange.GetUser.Loading) }
+ .catch { emit(PartialChange.GetUser.Error(it)) }
- val refreshChanges = refreshGetUsers::invoke
- .asFlow()
- .map { PartialChange.Refresh.Success as PartialChange.Refresh }
- .onStart { emit(PartialChange.Refresh.Loading) }
- .catch { emit(PartialChange.Refresh.Failure(it)) }
+ val refreshChanges = refreshGetUsers::invoke
+ .asFlow()
+ .map { PartialChange.Refresh.Success as PartialChange.Refresh }
+ .onStart { emit(PartialChange.Refresh.Loading) }
+ .catch { emit(PartialChange.Refresh.Failure(it)) }
- return merge(
- filterIsInstance()
- .logIntent()
- .flatMapConcat { getUserChanges },
- filterIsInstance()
- .filter { viewState.value.let { !it.isLoading && it.error === null } }
- .logIntent()
- .flatMapFirst { refreshChanges },
- filterIsInstance()
- .filter { viewState.value.error != null }
- .logIntent()
- .flatMapFirst { getUserChanges },
- filterIsInstance()
- .logIntent()
- .map { it.user }
- .flatMapMerge { userItem ->
- flow {
- userItem
- .toDomain()
- .let { removeUser(it) }
- .let { emit(it) }
+ return merge(
+ filterIsInstance()
+ .logIntent()
+ .flatMapConcat { getUserChanges },
+ filterIsInstance()
+ .filter { viewState.value.let { !it.isLoading && it.error === null } }
+ .logIntent()
+ .flatMapFirst { refreshChanges },
+ filterIsInstance()
+ .filter { viewState.value.error != null }
+ .logIntent()
+ .flatMapFirst { getUserChanges },
+ filterIsInstance()
+ .logIntent()
+ .map { it.user }
+ .flatMapMerge { userItem ->
+ flow {
+ userItem
+ .toDomain()
+ .let { removeUser(it) }
+ .let { emit(it) }
+ }
+ .map { PartialChange.RemoveUser.Success(userItem) as PartialChange.RemoveUser }
+ .catch { emit(PartialChange.RemoveUser.Failure(userItem, it)) }
}
- .map { PartialChange.RemoveUser.Success(userItem) as PartialChange.RemoveUser }
- .catch { emit(PartialChange.RemoveUser.Failure(userItem, it)) }
- }
- )
- }
+ )
+ }
private fun Flow.logIntent() = onEach { Log.d("MainVM", "## Intent: $it") }
}
+
+private fun defer(flowFactory: () -> Flow): Flow = flow { emitAll(flowFactory()) }
diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/ExampleUnitTest.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/ExampleUnitTest.kt
deleted file mode 100644
index bac2a27d..00000000
--- a/feature-main/src/test/java/com/hoc/flowmvi/ui/ExampleUnitTest.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.hoc.flowmvi.ui
-
-import org.junit.Test
-
-/**
- * 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)
- }
-}
diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt
new file mode 100644
index 00000000..5145d156
--- /dev/null
+++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt
@@ -0,0 +1,186 @@
+package com.hoc.flowmvi.ui.main
+
+import com.hoc.flowmvi.domain.entity.User
+import com.hoc.flowmvi.domain.usecase.GetUsersUseCase
+import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase
+import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase
+import io.mockk.clearAllMocks
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.setMain
+import java.io.IOException
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+
+val users = listOf(
+ User(
+ id = "1",
+ email = "email1@gmail.com",
+ firstName = "first1",
+ lastName = "last1",
+ avatar = "1.png"
+ ),
+ User(
+ id = "2",
+ email = "email1@gmail.com",
+ firstName = "first2",
+ lastName = "last2",
+ avatar = "2.png"
+ ),
+ User(
+ id = "3",
+ email = "email1@gmail.com",
+ firstName = "first3",
+ lastName = "last3",
+ avatar = "3.png"
+ ),
+)
+
+internal val usersItems = users.map { UserItem(it) }
+
+@ExperimentalCoroutinesApi
+@FlowPreview
+class MainVMTest {
+ private val testDispatcher = TestCoroutineDispatcher()
+
+ private lateinit var vm: MainVM
+ private val getUserUseCase: GetUsersUseCase = mockk(relaxed = true)
+ private val refreshGetUsersUseCase: RefreshGetUsersUseCase = mockk(relaxed = true)
+ private val removeUser: RemoveUserUseCase = mockk(relaxed = true)
+
+ @BeforeTest
+ fun setup() {
+ Dispatchers.setMain(testDispatcher)
+
+ vm = MainVM(
+ getUsersUseCase = getUserUseCase,
+ refreshGetUsers = refreshGetUsersUseCase,
+ removeUser = removeUser,
+ )
+ }
+
+ @AfterTest
+ fun teardown() {
+ Dispatchers.resetMain()
+ testDispatcher.cleanupTestCoroutines()
+ clearAllMocks()
+ }
+
+ @Test
+ fun `ViewIntent_Initial returns success`() = testDispatcher.runBlockingTest {
+ every { getUserUseCase() } returns flow {
+ delay(100)
+ emit(users)
+ }
+
+ val hasEvent = AtomicBoolean(false)
+ val eventJob = launch(start = CoroutineStart.UNDISPATCHED) {
+ vm.singleEvent.collect {
+ hasEvent.set(true)
+ }
+ }
+
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ vm.viewState
+ .take(2)
+ .toList()
+ .let {
+ assertContentEquals(
+ it,
+ listOf(
+ ViewState.initial(),
+ ViewState(
+ userItems = usersItems,
+ isLoading = false,
+ error = null,
+ isRefreshing = false
+ )
+ )
+ )
+ }
+
+ eventJob.cancel()
+ assertFalse(hasEvent.get())
+ verify(exactly = 1) { getUserUseCase() }
+
+ print("DONE")
+ cancel()
+ }
+
+ vm.processIntent(ViewIntent.Initial)
+ }
+
+ @Test
+ fun `ViewIntent_Initial returns failure`() = testDispatcher.runBlockingTest {
+ val ioException = IOException()
+
+ every { getUserUseCase() } returns flow {
+ delay(100)
+ throw ioException
+ }
+
+ val events = AtomicReference(emptyList())
+ val eventJob = launch(start = CoroutineStart.UNDISPATCHED) {
+ vm.singleEvent.collect { e ->
+ events.updateAndGet { it + e }
+ }
+ }
+
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ vm.viewState
+ .take(2)
+ .toList()
+ .let {
+ assertContentEquals(
+ it,
+ listOf(
+ ViewState.initial(),
+ ViewState(
+ userItems = emptyList(),
+ isLoading = false,
+ error = ioException,
+ isRefreshing = false
+ )
+ )
+ )
+ }
+
+ eventJob.cancel()
+
+ assertEquals(
+ events.get().single(),
+ SingleEvent.GetUsersError(
+ error = ioException,
+ ),
+ )
+
+ verify(exactly = 1) { getUserUseCase() }
+
+ print("DONE")
+ cancel()
+ }
+
+ vm.processIntent(ViewIntent.Initial)
+ }
+}
diff --git a/feature-search/build.gradle.kts b/feature-search/build.gradle.kts
index 979e47f9..c403df2c 100644
--- a/feature-search/build.gradle.kts
+++ b/feature-search/build.gradle.kts
@@ -30,8 +30,12 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() }
-
buildFeatures { viewBinding = true }
+
+ testOptions {
+ unitTests.isIncludeAndroidResources = true
+ unitTests.isReturnDefaultValues = true
+ }
}
dependencies {
@@ -54,4 +58,6 @@ dependencies {
implementation(deps.coil)
implementation(deps.viewBindingDelegate)
implementation(deps.flowExt)
+
+ addUnitTest()
}
diff --git a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/ExampleUnitTest.kt b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/ExampleUnitTest.kt
index 77fa3853..9d401fb8 100644
--- a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/ExampleUnitTest.kt
+++ b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/ExampleUnitTest.kt
@@ -1 +1,11 @@
package com.hoc.flowmvi.ui.search
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}