From 9081d451b9f93a465111975fca4eabadae2d4b8e Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Sat, 25 Dec 2021 23:30:40 +0700 Subject: [PATCH 01/10] kotlin 1.6.10, coroutines 1.6 --- .idea/compiler.xml | 2 +- .idea/misc.xml | 2 +- app/build.gradle.kts | 1 + app/src/main/java/com/hoc/flowmvi/App.kt | 3 +- .../java/com/hoc/flowmvi/CheckModulesTest.kt | 11 ++++-- buildSrc/src/main/kotlin/deps.kt | 4 +- .../hoc/flowmvi/data/UserRepositoryImpl.kt | 3 +- .../data/UserRepositoryImplRealAPITest.kt | 14 ++++--- .../flowmvi/data/UserRepositoryImplTest.kt | 37 +++++++++---------- .../com/hoc/flowmvi/domain/UseCaseTest.kt | 31 ++++++++-------- .../java/com/hoc/flowmvi/ui/add/AddVMTest.kt | 12 +++--- .../com/hoc/flowmvi/ui/main/MainVMTest.kt | 7 ++-- .../com/hoc/flowmvi/ui/search/SearchVM.kt | 3 +- .../com/hoc/flowmvi/ui/search/SearchVMTest.kt | 5 ++- .../mvi_testing/BaseMviViewModelTest.kt | 17 ++++----- .../test_utils/TestCoroutineDispatcherRule.kt | 6 +-- .../hoc/flowmvi/test_utils/TestDispatchers.kt | 4 +- 17 files changed, 85 insertions(+), 77 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 7bc1015c..ac477352 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,7 +1,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 72be2440..8f0a1f7f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -13,7 +13,7 @@ - + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 177bcfc4..0f04686a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -75,5 +75,6 @@ dependencies { androidTestImplementation(deps.test.androidXSspresso) addUnitTest() + testImplementation(testUtils) testImplementation(deps.koin.testJunit4) } diff --git a/app/src/main/java/com/hoc/flowmvi/App.kt b/app/src/main/java/com/hoc/flowmvi/App.kt index f703fc24..2b5ad671 100644 --- a/app/src/main/java/com/hoc/flowmvi/App.kt +++ b/app/src/main/java/com/hoc/flowmvi/App.kt @@ -47,7 +47,8 @@ class App : Application() { startKoin { androidContext(this@App) - androidLogger(if (BuildConfig.DEBUG) Level.DEBUG else Level.NONE) + // TODO(koin): https://github.com/InsertKoinIO/koin/issues/1188 + androidLogger(if (BuildConfig.DEBUG) Level.ERROR else Level.NONE) modules(allModules) } diff --git a/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt index c01656e4..22f6180b 100644 --- a/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt +++ b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt @@ -1,8 +1,11 @@ package com.hoc.flowmvi import androidx.lifecycle.SavedStateHandle +import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule import io.mockk.every import io.mockk.mockkClass +import kotlin.test.Test +import kotlin.time.ExperimentalTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import org.junit.Rule @@ -11,8 +14,6 @@ import org.koin.dsl.koinApplication import org.koin.test.AutoCloseKoinTest import org.koin.test.check.checkModules import org.koin.test.mock.MockProviderRule -import kotlin.test.Test -import kotlin.time.ExperimentalTime @ExperimentalStdlibApi @FlowPreview @@ -27,12 +28,16 @@ class CheckModulesTest : AutoCloseKoinTest() { } } } + @get:Rule + val coroutineRule = TestCoroutineDispatcherRule() @Test fun verifyKoinApp() { koinApplication { modules(allModules) - printLogger(Level.DEBUG) + + // TODO(koin): https://github.com/InsertKoinIO/koin/issues/1188 + printLogger(Level.ERROR) checkModules { withInstance() diff --git a/buildSrc/src/main/kotlin/deps.kt b/buildSrc/src/main/kotlin/deps.kt index 23fb411c..13027bca 100644 --- a/buildSrc/src/main/kotlin/deps.kt +++ b/buildSrc/src/main/kotlin/deps.kt @@ -7,7 +7,7 @@ import org.gradle.plugin.use.PluginDependenciesSpec import org.gradle.plugin.use.PluginDependencySpec const val ktlintVersion = "0.43.2" -const val kotlinVersion = "1.5.31" +const val kotlinVersion = "1.6.10" object appConfig { const val applicationId = "com.hoc.flowmvi" @@ -52,7 +52,7 @@ object deps { } object coroutines { - private const val version = "1.5.2" + private const val version = "1.6.0" const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version" const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version" diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index f38aad37..90ce4752 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -31,6 +31,7 @@ import java.io.IOException import kotlin.time.Duration import kotlin.time.ExperimentalTime import arrow.core.Either.Companion.catch as catchEither +import kotlin.time.Duration.Companion.milliseconds @ExperimentalTime @ExperimentalCoroutinesApi @@ -69,7 +70,7 @@ internal class UserRepositoryImpl( return withContext(dispatchers.io) { retrySuspend( times = 3, - initialDelay = Duration.milliseconds(500), + initialDelay = 500.milliseconds, factor = 2.0, shouldRetry = { it is IOException } ) { times -> diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt index cd2a2b68..359b0fc9 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt @@ -4,6 +4,11 @@ import android.util.Log import com.hoc.flowmvi.core.dispatchers.CoroutineDispatchers import com.hoc.flowmvi.domain.repository.UserRepository import com.hoc.flowmvi.test_utils.getOrThrow +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlin.time.ExperimentalTime import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main @@ -13,16 +18,12 @@ import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.rules.TestWatcher import org.junit.runner.Description +import org.koin.core.logger.Level import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.KoinTestRule import org.koin.test.inject import timber.log.Timber -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import kotlin.test.Test -import kotlin.test.assertTrue -import kotlin.time.ExperimentalTime @ExperimentalCoroutinesApi @ExperimentalTime @@ -30,7 +31,8 @@ import kotlin.time.ExperimentalTime class UserRepositoryImplRealAPITest : KoinTest { @get:Rule val koinRuleTest = KoinTestRule.create { - printLogger() + // TODO(koin): https://github.com/InsertKoinIO/koin/issues/1188 + printLogger(Level.ERROR) modules( dataModule, module { diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index 83376dfd..8977cccd 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -24,13 +24,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import io.mockk.verifySequence -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Rule import java.io.IOException import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -41,6 +34,13 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.time.ExperimentalTime +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Rule private val USER_BODY = UserBody( email = "email1@gmail.com", @@ -103,7 +103,6 @@ private val VALID_NEL_USERS = USERS.map(User::validNel) class UserRepositoryImplTest { @get:Rule val coroutineRule = TestCoroutineDispatcherRule() - private val testDispatcher get() = coroutineRule.testCoroutineDispatcher private lateinit var repo: UserRepositoryImpl private lateinit var userApiService: UserApiService @@ -139,7 +138,7 @@ class UserRepositoryImplTest { } @Test - fun test_refresh_withApiCallSuccess_returnsRight() = testDispatcher.runBlockingTest { + fun test_refresh_withApiCallSuccess_returnsRight() = runTest { coEvery { userApiService.getUsers() } returns USER_RESPONSES every { responseToDomain(any()) } returnsMany VALID_NEL_USERS @@ -157,7 +156,7 @@ class UserRepositoryImplTest { } @Test - fun test_refresh_withApiCallError_returnsLeft() = testDispatcher.runBlockingTest { + fun test_refresh_withApiCallError_returnsLeft() = runTest { val ioException = IOException() coEvery { userApiService.getUsers() } throws ioException every { errorMapper(ofType()) } returns UserError.NetworkError @@ -171,7 +170,7 @@ class UserRepositoryImplTest { } @Test - fun test_remove_withApiCallSuccess_returnsRight() = testDispatcher.runBlockingTest { + fun test_remove_withApiCallSuccess_returnsRight() = runTest { val user = USERS[0] val userResponse = USER_RESPONSES[0] @@ -188,7 +187,7 @@ class UserRepositoryImplTest { } @Test - fun test_remove_withApiCallError_returnsLeft() = testDispatcher.runBlockingTest { + fun test_remove_withApiCallError_returnsLeft() = runTest { val user = USERS[0] coEvery { userApiService.remove(user.id) } throws IOException() every { errorMapper(ofType()) } returns UserError.NetworkError @@ -202,7 +201,7 @@ class UserRepositoryImplTest { } @Test - fun test_add_withApiCallSuccess_returnsRight() = testDispatcher.runBlockingTest { + fun test_add_withApiCallSuccess_returnsRight() = runTest { val user = USERS[0] val userResponse = USER_RESPONSES[0] @@ -221,7 +220,7 @@ class UserRepositoryImplTest { } @Test - fun test_add_withApiCallError_returnsLeft() = testDispatcher.runBlockingTest { + fun test_add_withApiCallError_returnsLeft() = runTest { val user = USERS[0] coEvery { userApiService.add(USER_BODY) } throws IOException() every { domainToBody(user) } returns USER_BODY @@ -238,7 +237,7 @@ class UserRepositoryImplTest { } @Test - fun test_search_withApiCallSuccess_returnsRight() = testDispatcher.runBlockingTest { + fun test_search_withApiCallSuccess_returnsRight() = runTest { val q = "hoc081098" coEvery { userApiService.search(q) } returns USER_RESPONSES every { responseToDomain(any()) } returnsMany VALID_NEL_USERS @@ -258,7 +257,7 @@ class UserRepositoryImplTest { } @Test - fun test_search_withApiCallError_returnsLeft() = testDispatcher.runBlockingTest { + fun test_search_withApiCallError_returnsLeft() = runTest { val q = "hoc081098" coEvery { userApiService.search(q) } throws IOException() every { errorMapper(ofType()) } returns UserError.NetworkError @@ -273,7 +272,7 @@ class UserRepositoryImplTest { } @Test - fun test_getUsers_withApiCallSuccess_emitsInitial() = testDispatcher.runBlockingTest { + fun test_getUsers_withApiCallSuccess_emitsInitial() = runTest { coEvery { userApiService.getUsers() } returns USER_RESPONSES every { responseToDomain(any()) } returnsMany VALID_NEL_USERS @@ -299,7 +298,7 @@ class UserRepositoryImplTest { } @Test - fun test_getUsers_withApiCallError_rethrows() = testDispatcher.runBlockingTest { + fun test_getUsers_withApiCallError_rethrows() = runTest { coEvery { userApiService.getUsers() } throws IOException() every { errorMapper(ofType()) } returns UserError.NetworkError @@ -322,7 +321,7 @@ class UserRepositoryImplTest { @Test fun test_getUsers_withApiCallSuccess_emitsInitialAndUpdatedUsers() = - testDispatcher.runBlockingTest { + runTest { val user = USERS.last() val userResponse = USER_RESPONSES.last() coEvery { userApiService.getUsers() } returns USER_RESPONSES.dropLast(1) diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt index 9082db08..a0bbb45e 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt @@ -19,15 +19,15 @@ import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runBlockingTest -import org.junit.Rule import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Rule private val USERS = listOf( User.create( @@ -57,7 +57,6 @@ private val USERS = listOf( class UseCaseTest { @get:Rule val coroutineRule = TestCoroutineDispatcherRule() - private val testDispatcher get() = coroutineRule.testCoroutineDispatcher private lateinit var userRepository: UserRepository private lateinit var getUsersUseCase: GetUsersUseCase @@ -86,7 +85,7 @@ class UseCaseTest { } @Test - fun test_getUsersUseCase_whenSuccess_emitsUsers() = testDispatcher.runBlockingTest { + fun test_getUsersUseCase_whenSuccess_emitsUsers() = runTest { val usersRight = USERS.right() every { userRepository.getUsers() } returns flowOf(usersRight) @@ -97,7 +96,7 @@ class UseCaseTest { } @Test - fun test_getUsersUseCase_whenError_throwsError() = testDispatcher.runBlockingTest { + fun test_getUsersUseCase_whenError_throwsError() = runTest { every { userRepository.getUsers() } returns flowOf(errorLeft) val result = getUsersUseCase() @@ -107,7 +106,7 @@ class UseCaseTest { } @Test - fun test_refreshUseCase_whenSuccess_returnsUnit() = testDispatcher.runBlockingTest { + fun test_refreshUseCase_whenSuccess_returnsUnit() = runTest { coEvery { userRepository.refresh() } returns Unit.right() val result = refreshUseCase() @@ -117,7 +116,7 @@ class UseCaseTest { } @Test - fun test_refreshUseCase_whenError_throwsError() = testDispatcher.runBlockingTest { + fun test_refreshUseCase_whenError_throwsError() = runTest { coEvery { userRepository.refresh() } returns errorLeft val result = refreshUseCase() @@ -127,7 +126,7 @@ class UseCaseTest { } @Test - fun test_removeUserUseCase_whenSuccess_returnsUnit() = testDispatcher.runBlockingTest { + fun test_removeUserUseCase_whenSuccess_returnsUnit() = runTest { coEvery { userRepository.remove(any()) } returns Unit.right() val result = removeUserUseCase(USERS[0]) @@ -137,7 +136,7 @@ class UseCaseTest { } @Test - fun test_removeUserUseCase_whenError_throwsError() = testDispatcher.runBlockingTest { + fun test_removeUserUseCase_whenError_throwsError() = runTest { coEvery { userRepository.remove(any()) } returns errorLeft val result = removeUserUseCase(USERS[0]) @@ -147,7 +146,7 @@ class UseCaseTest { } @Test - fun test_addUserUseCase_whenSuccess_returnsUnit() = testDispatcher.runBlockingTest { + fun test_addUserUseCase_whenSuccess_returnsUnit() = runTest { coEvery { userRepository.add(any()) } returns Unit.right() val result = addUserUseCase(USERS[0]) @@ -157,7 +156,7 @@ class UseCaseTest { } @Test - fun test_addUserUseCase_whenError_throwsError() = testDispatcher.runBlockingTest { + fun test_addUserUseCase_whenError_throwsError() = runTest { coEvery { userRepository.add(any()) } returns errorLeft val result = addUserUseCase(USERS[0]) @@ -167,7 +166,7 @@ class UseCaseTest { } @Test - fun test_searchUsersUseCase_whenSuccess_returnsUsers() = testDispatcher.runBlockingTest { + fun test_searchUsersUseCase_whenSuccess_returnsUsers() = runTest { coEvery { userRepository.search(any()) } returns USERS.right() val query = "hoc081098" @@ -178,7 +177,7 @@ class UseCaseTest { } @Test - fun test_searchUsersUseCase_whenError_throwsError() = testDispatcher.runBlockingTest { + fun test_searchUsersUseCase_whenError_throwsError() = runTest { coEvery { userRepository.search(any()) } returns errorLeft val query = "hoc081098" diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt index 6497f36d..cd518aa9 100644 --- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt +++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt @@ -16,11 +16,11 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.confirmVerified import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf import kotlin.test.Test -import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf private val ALL_ERRORS = UserValidationError.values().toSet() private const val EMAIL = "hoc081098@gmail.com" @@ -322,7 +322,7 @@ class AddVMTest : BaseMviViewModelTest(ViewIntent.Retry).concatWith( flow { delay(SEMI_TIMEOUT) // (2) very short ... emit(ViewIntent.Search(query2)) @@ -576,7 +577,7 @@ class SearchVMTest : BaseMviViewModelTest? = null, otherAssertions: (suspend () -> Unit)? = null, - ) = testDispatcher.runBlockingTest { + ) = runTest { fun logIfEnabled(s: () -> String) = if (logging) println(s()) else Unit val vm = vmProducer() diff --git a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestCoroutineDispatcherRule.kt b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestCoroutineDispatcherRule.kt index 1b25e277..63f89094 100644 --- a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestCoroutineDispatcherRule.kt +++ b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestCoroutineDispatcherRule.kt @@ -2,14 +2,15 @@ package com.hoc.flowmvi.test_utils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.rules.TestWatcher import org.junit.runner.Description @ExperimentalCoroutinesApi -class TestCoroutineDispatcherRule(val testCoroutineDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : +class TestCoroutineDispatcherRule(val testCoroutineDispatcher: TestDispatcher = StandardTestDispatcher()) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(testCoroutineDispatcher) @@ -17,6 +18,5 @@ class TestCoroutineDispatcherRule(val testCoroutineDispatcher: TestCoroutineDisp override fun finished(description: Description) { Dispatchers.resetMain() - testCoroutineDispatcher.cleanupTestCoroutines() } } diff --git a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestDispatchers.kt b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestDispatchers.kt index 5faa6744..c5fb3d7e 100644 --- a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestDispatchers.kt +++ b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestDispatchers.kt @@ -3,10 +3,10 @@ package com.hoc.flowmvi.test_utils import com.hoc.flowmvi.core.dispatchers.CoroutineDispatchers import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.TestDispatcher @ExperimentalCoroutinesApi -class TestDispatchers(testCoroutineDispatcher: TestCoroutineDispatcher) : +class TestDispatchers(testCoroutineDispatcher: TestDispatcher) : CoroutineDispatchers { override val main: CoroutineDispatcher = testCoroutineDispatcher override val io: CoroutineDispatcher = testCoroutineDispatcher From c0a4df7af07a15a59d16e81d4c6e8165981a2a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Sat, 25 Dec 2021 23:38:46 +0700 Subject: [PATCH 02/10] Update app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt index 22f6180b..0f44b893 100644 --- a/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt +++ b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt @@ -4,8 +4,6 @@ import androidx.lifecycle.SavedStateHandle import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule import io.mockk.every import io.mockk.mockkClass -import kotlin.test.Test -import kotlin.time.ExperimentalTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import org.junit.Rule From e8861af7153cd9648494629d66d15db6b1989176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Sat, 25 Dec 2021 23:38:54 +0700 Subject: [PATCH 03/10] Update app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt index 0f44b893..921dfa51 100644 --- a/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt +++ b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt @@ -12,6 +12,8 @@ import org.koin.dsl.koinApplication import org.koin.test.AutoCloseKoinTest import org.koin.test.check.checkModules import org.koin.test.mock.MockProviderRule +import kotlin.test.Test +import kotlin.time.ExperimentalTime @ExperimentalStdlibApi @FlowPreview From 4edab5e90f2641d4178fca1ec066a070010578e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Sat, 25 Dec 2021 23:39:01 +0700 Subject: [PATCH 04/10] Update data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index 90ce4752..1da06659 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -31,7 +31,6 @@ import java.io.IOException import kotlin.time.Duration import kotlin.time.ExperimentalTime import arrow.core.Either.Companion.catch as catchEither -import kotlin.time.Duration.Companion.milliseconds @ExperimentalTime @ExperimentalCoroutinesApi From 85d5c5de3ffb67b80aedcc4f8a1aef5cc552fa78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Sat, 25 Dec 2021 23:39:16 +0700 Subject: [PATCH 05/10] Update data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt index 359b0fc9..a5419354 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt @@ -4,11 +4,6 @@ import android.util.Log import com.hoc.flowmvi.core.dispatchers.CoroutineDispatchers import com.hoc.flowmvi.domain.repository.UserRepository import com.hoc.flowmvi.test_utils.getOrThrow -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import kotlin.test.Test -import kotlin.test.assertTrue -import kotlin.time.ExperimentalTime import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main From 4196c20ecc3bf2289ec91364c9e8b561c88567a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Sat, 25 Dec 2021 23:39:30 +0700 Subject: [PATCH 06/10] Update data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt index a5419354..9e0c8436 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt @@ -19,6 +19,11 @@ import org.koin.test.KoinTest import org.koin.test.KoinTestRule import org.koin.test.inject import timber.log.Timber +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlin.time.ExperimentalTime @ExperimentalCoroutinesApi @ExperimentalTime From 86e235c99048194d37b7af369d5fbcc85b98bfac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petrus=20Nguy=E1=BB=85n=20Th=C3=A1i=20H=E1=BB=8Dc?= Date: Sat, 25 Dec 2021 23:39:45 +0700 Subject: [PATCH 07/10] Update mvi/mvi-testing/src/main/java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mvi/mvi-testing/src/main/java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt b/mvi/mvi-testing/src/main/java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt index 1d5e01f9..45a025fb 100644 --- a/mvi/mvi-testing/src/main/java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt +++ b/mvi/mvi-testing/src/main/java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt @@ -25,6 +25,11 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Rule +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.assertEquals +import kotlin.time.Duration +import kotlin.time.ExperimentalTime @ExperimentalTime @ExperimentalCoroutinesApi From 1830be6eb4e85a3d87ccb3e5b7da18e252fe0748 Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Sat, 25 Dec 2021 23:43:50 +0700 Subject: [PATCH 08/10] spotless --- .../com/hoc/flowmvi/data/UserRepositoryImpl.kt | 1 - .../com/hoc/flowmvi/data/UserRepositoryImplTest.kt | 14 +++++++------- .../java/com/hoc/flowmvi/domain/UseCaseTest.kt | 8 ++++---- .../test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt | 4 ++-- .../java/com/hoc/flowmvi/ui/main/MainVMTest.kt | 1 - .../java/com/hoc/flowmvi/ui/search/SearchVM.kt | 1 - .../java/com/hoc/flowmvi/ui/search/SearchVMTest.kt | 1 - .../flowmvi/mvi_testing/BaseMviViewModelTest.kt | 5 ----- 8 files changed, 13 insertions(+), 22 deletions(-) diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index 1da06659..86705c27 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.scan import kotlinx.coroutines.withContext import timber.log.Timber import java.io.IOException -import kotlin.time.Duration import kotlin.time.ExperimentalTime import arrow.core.Either.Companion.catch as catchEither diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index 8977cccd..ae430c3f 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -24,6 +24,13 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import io.mockk.verifySequence +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Rule import java.io.IOException import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -34,13 +41,6 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.time.ExperimentalTime -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest -import org.junit.Rule private val USER_BODY = UserBody( email = "email1@gmail.com", diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt index a0bbb45e..af430476 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt @@ -19,15 +19,15 @@ import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Rule +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals private val USERS = listOf( User.create( diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt index cd518aa9..b63bae97 100644 --- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt +++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt @@ -16,11 +16,11 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.confirmVerified import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlin.test.Test import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf private val ALL_ERRORS = UserValidationError.values().toSet() private const val EMAIL = "hoc081098@gmail.com" 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 index 377e9563..bb6dce5d 100644 --- 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 @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.update import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs -import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchVM.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchVM.kt index 7075f892..86af3af4 100644 --- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchVM.kt +++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchVM.kt @@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn import timber.log.Timber -import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime diff --git a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchVMTest.kt b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchVMTest.kt index c3b428e3..9980b897 100644 --- a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchVMTest.kt +++ b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchVMTest.kt @@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlin.test.Test -import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime diff --git a/mvi/mvi-testing/src/main/java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt b/mvi/mvi-testing/src/main/java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt index 45a025fb..19265fb7 100644 --- a/mvi/mvi-testing/src/main/java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt +++ b/mvi/mvi-testing/src/main/java/com/hoc/flowmvi/mvi_testing/BaseMviViewModelTest.kt @@ -9,11 +9,6 @@ import com.hoc.flowmvi.mvi_base.MviViewModel import com.hoc.flowmvi.mvi_base.MviViewState import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule import io.mockk.clearAllMocks -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.assertEquals -import kotlin.time.Duration -import kotlin.time.ExperimentalTime import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay From 308e0174d17b9742ace7f51e87b5ac6afbccc7e7 Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Sat, 25 Dec 2021 23:54:54 +0700 Subject: [PATCH 09/10] fix --- data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index 86705c27..ae3de8d9 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.scan import kotlinx.coroutines.withContext import timber.log.Timber import java.io.IOException +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime import arrow.core.Either.Companion.catch as catchEither From 14f4c9965e4587ff27a4a09791ed5e36c8b64ddd Mon Sep 17 00:00:00 2001 From: Petrus Nguyen Thai Hoc Date: Tue, 18 Jan 2022 01:25:44 +0700 Subject: [PATCH 10/10] fix vm tests --- .../flowmvi/data/UserRepositoryImplTest.kt | 2 +- .../java/com/hoc/flowmvi/ui/add/AddVMTest.kt | 51 ++-- .../com/hoc/flowmvi/ui/main/MainVMTest.kt | 158 +++++----- .../com/hoc/flowmvi/ui/search/SearchVMTest.kt | 52 ++-- .../mvi_testing/BaseMviViewModelTest.kt | 270 +++++++++++++----- .../test_utils/TestCoroutineDispatcherRule.kt | 10 +- 6 files changed, 335 insertions(+), 208 deletions(-) diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index ae430c3f..7187582a 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -119,7 +119,7 @@ class UserRepositoryImplTest { repo = UserRepositoryImpl( userApiService = userApiService, - dispatchers = TestDispatchers(coroutineRule.testCoroutineDispatcher), + dispatchers = TestDispatchers(coroutineRule.testDispatcher), responseToDomain = responseToDomain, domainToBody = domainToBody, errorMapper = errorMapper diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt index b63bae97..47d2650c 100644 --- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt +++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt @@ -11,6 +11,7 @@ import com.hoc.flowmvi.domain.model.UserValidationError.TOO_SHORT_LAST_NAME import com.hoc.flowmvi.domain.usecase.AddUserUseCase import com.hoc.flowmvi.mvi_testing.BaseMviViewModelTest import com.hoc.flowmvi.mvi_testing.mapRight +import com.hoc.flowmvi.mvi_testing.returnsWithDelay import com.hoc.flowmvi.test_utils.valueOrThrow import io.mockk.coEvery import io.mockk.coVerify @@ -19,7 +20,6 @@ import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlin.test.Test -import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime private val ALL_ERRORS = UserValidationError.values().toSet() @@ -55,7 +55,7 @@ class AddVMTest : BaseMviViewModelTest(ViewIntent.Retry).concatWith( flow { @@ -561,11 +554,10 @@ class SearchVMTest : BaseMviViewModelTest Iterable.mapRight(): List Unit, T>> = map { it.right() } + +/** + * Workaround for [Kotlin/kotlinx.coroutines/issues/3120](https://github.com/Kotlin/kotlinx.coroutines/issues/3120). + * TODO(coroutines): https://github.com/Kotlin/kotlinx.coroutines/issues/3120 + */ +fun Flow.delayEach() = onEach { delay(1) } + +/** + * Workaround for [Kotlin/kotlinx.coroutines/issues/3120](https://github.com/Kotlin/kotlinx.coroutines/issues/3120). + * TODO(coroutines): https://github.com/Kotlin/kotlinx.coroutines/issues/3120 + */ +infix fun MockKStubScope.returnsWithDelay(returnValue: T): MockKAdditionalAnswerScope = + coAnswers { + delay(1) + returnValue + } + +/** + * Workaround for [Kotlin/kotlinx.coroutines/issues/3120](https://github.com/Kotlin/kotlinx.coroutines/issues/3120). + * TODO(coroutines): https://github.com/Kotlin/kotlinx.coroutines/issues/3120 + */ +infix fun MockKStubScope.returnsManyWithDelay(values: List) { + var count = 0 + coAnswers { + delay(1) + values[count++] + } +} + @ExperimentalTime @ExperimentalCoroutinesApi abstract class BaseMviViewModelTest< @@ -35,7 +70,11 @@ abstract class BaseMviViewModelTest< VM : MviViewModel, > { @get:Rule - val coroutineRule = TestCoroutineDispatcherRule() + val coroutineRule = TestCoroutineDispatcherRule( + testDispatcher = UnconfinedTestDispatcher( + name = "${this::class.java.simpleName}-UnconfinedTestDispatcher", + ) + ) @CallSuper @BeforeTest @@ -48,90 +87,183 @@ abstract class BaseMviViewModelTest< clearAllMocks() } - protected fun test( + protected fun runVMTest( vmProducer: () -> VM, intents: Flow, expectedStates: List Unit, S>>, expectedEvents: List Unit, E>>, - delayAfterDispatchingIntents: Duration = Duration.ZERO, - logging: Boolean = BuildConfig.ENABLE_LOG_TEST, - intentsBeforeCollecting: Flow? = null, + loggingEnabled: Boolean = BuildConfig.ENABLE_LOG_TEST, + preProcessingIntents: Flow? = null, otherAssertions: (suspend () -> Unit)? = null, - ) = runTest { - fun logIfEnabled(s: () -> String) = if (logging) println(s()) else Unit - + ) = runTest(coroutineRule.testDispatcher) { val vm = vmProducer() - intentsBeforeCollecting?.let { flow -> - val job = vm.singleEvent.launchIn(this) // ignore events - - flow - .onCompletion { - job.cancel() - logIfEnabled { "-".repeat(32) } - } - .collect { - vm.processIntent(it) - logIfEnabled { "[BEFORE] Dispatch $it -> $vm" } - } - } + preProcessingIntents?.let { prepare(vm, it, loggingEnabled) } - logIfEnabled { "[START] $vm" } + // ------------------------------------- RUN ------------------------------------- - val states = mutableListOf() - val events = mutableListOf() + val (states, events, stateJob, eventJob) = run( + vm = vm, + intents = intents, + expectedStates = expectedStates, + expectedEvents = expectedEvents, + loggingEnabled = loggingEnabled, + ) - val stateJob = launch(start = CoroutineStart.UNDISPATCHED) { - vm.viewState.onEach { logIfEnabled { "[STATE] <- $it" } }.toList(states) - } - val eventJob = launch(start = CoroutineStart.UNDISPATCHED) { vm.singleEvent.toList(events) } + // ------------------------------------- DONE ------------------------------------- + + advanceUntilIdle() + runCurrent() + + stateJob.cancelAndJoin() + eventJob.cancelAndJoin() + + logIfEnabled(loggingEnabled) { DIVIDER } + logIfEnabled(loggingEnabled) { "[DONE] states=${states.joinToStringWithIndex()}" } + logIfEnabled(loggingEnabled) { "[DONE] events=${events.joinToStringWithIndex()}" } + + // ------------------------------------- ASSERTIONS ------------------------------------- + + assertEquals(expectedStates.size, states.size, "States size") + assertEquals(expectedEvents.size, events.size, "Events size") + doAssertions(expectedStates, states, expectedEvents, events) + + otherAssertions?.invoke() + } + + private suspend fun TestScope.run( + vm: VM, + intents: Flow, + expectedStates: List Unit, S>>, + expectedEvents: List Unit, E>>, + loggingEnabled: Boolean, + ): RunResult { + logIfEnabled(loggingEnabled) { "[START] $vm" } + + val states = LinkedList() + val events = LinkedList() + var stateIndex = 0 + var eventIndex = 0 + + val stateJob = vm.viewState + .onEach { state -> + logIfEnabled(loggingEnabled) { "[STATE] <- $state" } + + states += state + expectedStates[stateIndex].fold( + ifRight = { + assertEquals( + expected = it, + actual = state, + message = "[State index=$stateIndex]" + ) + }, + ifLeft = { it(state) } + ) + ++stateIndex + } + .launchIn(this) + + val eventJob = vm.singleEvent + .onEach { event -> + logIfEnabled(loggingEnabled) { "[EVENT] <- $event" } + + events += event + expectedEvents[eventIndex].fold( + ifRight = { + assertEquals( + expected = it, + actual = event, + message = "[Event index=$eventIndex]" + ) + }, + ifLeft = { it(event) } + ) + ++eventIndex + } + .launchIn(this) intents.collect { - logIfEnabled { "[DISPATCH] Dispatch $it -> $vm" } + logIfEnabled(loggingEnabled) { "[DISPATCH] Dispatch $it -> $vm" } vm.processIntent(it) } - delay(delayAfterDispatchingIntents) - logIfEnabled { "-".repeat(32) } - stateJob.cancel() - eventJob.cancel() - logIfEnabled { "[DONE] states=${states.joinToStringWithIndex()}" } - logIfEnabled { "[DONE] events=${events.joinToStringWithIndex()}" } + return RunResult( + states = states, + events = events, + stateJob = stateJob, + eventJob = eventJob, + ) + } - assertEquals(expectedStates.size, states.size, "States size") - expectedStates.withIndex().zip(states).forEach { (indexedValue, state) -> - val (index, exp) = indexedValue - exp.fold( - ifRight = { - assertEquals( - expected = it, - actual = state, - message = "[State index=$index]" - ) - }, - ifLeft = { it(state) } - ) - } + private suspend fun TestScope.prepare( + vm: VM, + intents: Flow, + loggingEnabled: Boolean, + ) { + val job1 = vm.viewState.launchIn(this) + val job2 = vm.singleEvent.launchIn(this) // ignore events - assertEquals(expectedEvents.size, events.size, "Events size") - expectedEvents.withIndex().zip(events).forEach { (indexedValue, event) -> - val (index, exp) = indexedValue - exp.fold( - ifRight = { - assertEquals( - expected = it, - actual = event, - message = "[Event index=$index]" - ) - }, - ifLeft = { it(event) } - ) - } + intents + .onCompletion { + advanceUntilIdle() + runCurrent() - otherAssertions?.invoke() + job1.cancelAndJoin() + job2.cancelAndJoin() + logIfEnabled(loggingEnabled) { DIVIDER } + } + .collect { + vm.processIntent(it) + logIfEnabled(loggingEnabled) { "[BEFORE] Dispatch $it -> $vm" } + } } } -fun Iterable.mapRight(): List Unit, T>> = map { it.right() } +private data class RunResult( + val states: List, + val events: List, + val stateJob: Job, + val eventJob: Job, +) + +private val DIVIDER = "-".repeat(32) + +private fun logIfEnabled(logging: Boolean, s: () -> String) = if (logging) println(s()) else Unit + +private fun doAssertions( + expectedStates: List Unit, S>>, + states: List, + expectedEvents: List Unit, E>>, + events: List, +) { + expectedStates.withIndex().zip(states).forEach { (indexedValue, state) -> + val (index, exp) = indexedValue + exp.fold( + ifRight = { + assertEquals( + expected = it, + actual = state, + message = "[State index=$index]" + ) + }, + ifLeft = { it(state) } + ) + } + + expectedEvents.withIndex().zip(events).forEach { (indexedValue, event) -> + val (index, exp) = indexedValue + exp.fold( + ifRight = { + assertEquals( + expected = it, + actual = event, + message = "[Event index=$index]" + ) + }, + ifLeft = { it(event) } + ) + } +} private fun List.joinToStringWithIndex(): String { return if (isEmpty()) "[]" else withIndex().joinToString( diff --git a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestCoroutineDispatcherRule.kt b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestCoroutineDispatcherRule.kt index 63f89094..f813243c 100644 --- a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestCoroutineDispatcherRule.kt +++ b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/TestCoroutineDispatcherRule.kt @@ -9,11 +9,15 @@ import kotlinx.coroutines.test.setMain import org.junit.rules.TestWatcher import org.junit.runner.Description +/** + * A test rule that sets the Main coroutine dispatcher for unit testing. + */ @ExperimentalCoroutinesApi -class TestCoroutineDispatcherRule(val testCoroutineDispatcher: TestDispatcher = StandardTestDispatcher()) : - TestWatcher() { +class TestCoroutineDispatcherRule( + val testDispatcher: TestDispatcher = StandardTestDispatcher(), +) : TestWatcher() { override fun starting(description: Description) { - Dispatchers.setMain(testCoroutineDispatcher) + Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) {