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) + } +}