diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseViewModel.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseViewModel.kt index d6a8b3bf2..85c95dfeb 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseViewModel.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseViewModel.kt @@ -17,8 +17,8 @@ abstract class BaseViewModel : ViewModel() { private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading - protected val _error = MutableStateFlow(null) - val error: StateFlow = _error + protected val _error = MutableSharedFlow() + val error: SharedFlow = _error protected val _navigator = MutableSharedFlow() val navigator: SharedFlow = _navigator diff --git a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeScreen.kt b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeScreen.kt index 31d94ed52..2858d05b3 100644 --- a/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeScreen.kt +++ b/sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/home/HomeScreen.kt @@ -28,15 +28,14 @@ fun HomeScreen( viewModel: HomeViewModel = hiltViewModel(), navigator: (destination: AppDestination) -> Unit, ) { + val context = LocalContext.current + viewModel.error.collectAsEffect { e -> e.showToast(context) } viewModel.navigator.collectAsEffect { destination -> navigator(destination) } val isLoading: IsLoading by viewModel.isLoading.collectAsStateWithLifecycle() val uiModels: List by viewModel.uiModels.collectAsStateWithLifecycle() val isFirstTimeLaunch: Boolean by viewModel.isFirstTimeLaunch.collectAsStateWithLifecycle() - val context = LocalContext.current - val error: Throwable? by viewModel.error.collectAsStateWithLifecycle() - error?.showToast(context) LaunchedEffect(isFirstTimeLaunch) { if (isFirstTimeLaunch) { context.showToast("This is the first time launch") diff --git a/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/CoroutineTestRule.kt b/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/CoroutineTestRule.kt index e918c00bb..d69163d51 100644 --- a/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/CoroutineTestRule.kt +++ b/sample-compose/app/src/test/java/co/nimblehq/sample/compose/test/CoroutineTestRule.kt @@ -8,7 +8,7 @@ import org.junit.runner.Description @OptIn(ExperimentalCoroutinesApi::class) class CoroutineTestRule( - private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() + var testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { val testDispatcherProvider = object : DispatchersProvider { diff --git a/sample-compose/app/src/test/java/co/nimblehq/sample/compose/ui/screens/BaseScreenTest.kt b/sample-compose/app/src/test/java/co/nimblehq/sample/compose/ui/screens/BaseScreenTest.kt new file mode 100644 index 000000000..72ba600f8 --- /dev/null +++ b/sample-compose/app/src/test/java/co/nimblehq/sample/compose/ui/screens/BaseScreenTest.kt @@ -0,0 +1,17 @@ +package co.nimblehq.sample.compose.ui.screens + +import co.nimblehq.sample.compose.test.CoroutineTestRule +import kotlinx.coroutines.test.StandardTestDispatcher + +abstract class BaseScreenTest { + + protected val coroutinesRule = CoroutineTestRule() + + protected fun setStandardTestDispatcher() { + coroutinesRule.testDispatcher = StandardTestDispatcher() + } + + protected fun advanceUntilIdle() { + coroutinesRule.testDispatcher.scheduler.advanceUntilIdle() + } +} diff --git a/sample-compose/app/src/test/java/co/nimblehq/sample/compose/ui/screens/home/HomeScreenTest.kt b/sample-compose/app/src/test/java/co/nimblehq/sample/compose/ui/screens/home/HomeScreenTest.kt index 694c0790f..2b508ba2b 100644 --- a/sample-compose/app/src/test/java/co/nimblehq/sample/compose/ui/screens/home/HomeScreenTest.kt +++ b/sample-compose/app/src/test/java/co/nimblehq/sample/compose/ui/screens/home/HomeScreenTest.kt @@ -9,8 +9,8 @@ import androidx.test.rule.GrantPermissionRule import co.nimblehq.sample.compose.R import co.nimblehq.sample.compose.domain.model.Model import co.nimblehq.sample.compose.domain.usecase.* -import co.nimblehq.sample.compose.test.CoroutineTestRule import co.nimblehq.sample.compose.ui.AppDestination +import co.nimblehq.sample.compose.ui.screens.BaseScreenTest import co.nimblehq.sample.compose.ui.screens.MainActivity import co.nimblehq.sample.compose.ui.theme.ComposeTheme import io.kotest.matchers.shouldBe @@ -26,9 +26,7 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.shadows.ShadowToast @RunWith(RobolectricTestRunner::class) -class HomeScreenTest { - - private val coroutinesRule = CoroutineTestRule() +class HomeScreenTest : BaseScreenTest() { @get:Rule val composeRule = createAndroidComposeRule() @@ -42,8 +40,10 @@ class HomeScreenTest { ) private val mockGetModelsUseCase: GetModelsUseCase = mockk() - private val mockIsFirstTimeLaunchPreferencesUseCase: IsFirstTimeLaunchPreferencesUseCase = mockk() - private val mockUpdateFirstTimeLaunchPreferencesUseCase: UpdateFirstTimeLaunchPreferencesUseCase = mockk() + private val mockIsFirstTimeLaunchPreferencesUseCase: IsFirstTimeLaunchPreferencesUseCase = + mockk() + private val mockUpdateFirstTimeLaunchPreferencesUseCase: UpdateFirstTimeLaunchPreferencesUseCase = + mockk() private lateinit var viewModel: HomeViewModel private var expectedAppDestination: AppDestination? = null @@ -54,8 +54,6 @@ class HomeScreenTest { listOf(Model(1), Model(2), Model(3)) ) every { mockIsFirstTimeLaunchPreferencesUseCase() } returns flowOf(false) - - initViewModel() } @Test @@ -70,12 +68,14 @@ class HomeScreenTest { @Test fun `When entering the Home screen and loading the list item failure, it shows the corresponding error`() { + setStandardTestDispatcher() + val error = Exception() every { mockGetModelsUseCase() } returns flow { throw error } - initViewModel() initComposable { - onNodeWithText("Home").assertIsDisplayed() + composeRule.waitForIdle() + advanceUntilIdle() ShadowToast.showedToast(activity.getString(R.string.error_generic)) shouldBe true } @@ -89,13 +89,15 @@ class HomeScreenTest { } private fun initComposable( - testBody: AndroidComposeTestRule, MainActivity>.() -> Unit + testBody: AndroidComposeTestRule, MainActivity>.() -> Unit, ) { + initViewModel() + composeRule.activity.setContent { ComposeTheme { HomeScreen( viewModel = viewModel, - navigator = { destination -> expectedAppDestination = destination } + navigator = { destination -> expectedAppDestination = destination }, ) } } diff --git a/sample-compose/buildSrc/src/main/java/Versions.kt b/sample-compose/buildSrc/src/main/java/Versions.kt index 1ad3cd49e..7ef68e4d6 100644 --- a/sample-compose/buildSrc/src/main/java/Versions.kt +++ b/sample-compose/buildSrc/src/main/java/Versions.kt @@ -9,17 +9,16 @@ object Versions { const val ANDROID_VERSION_NAME = "3.20.0" // Dependencies (Alphabet sorted) - const val ACCOMPANIST_PERMISSIONS_VERSION = "0.28.0" + const val ACCOMPANIST_PERMISSIONS_VERSION = "0.30.1" const val ANDROID_COMMON_KTX_VERSION = "0.1.1" const val ANDROID_CRYPTO_VERSION = "1.0.0" - const val ANDROIDX_CORE_KTX_VERSION = "1.9.0" + const val ANDROIDX_CORE_KTX_VERSION = "1.10.1" const val ANDROIDX_DATASTORE_PREFERENCES_VERSION = "1.0.0" - const val ANDROIDX_LIFECYCLE_VERSION = "2.6.0-rc01" - const val ANDROIDX_TEST_CORE_VERSION = "1.4.0" + const val ANDROIDX_LIFECYCLE_VERSION = "2.6.1" const val CHUCKER_VERSION = "3.5.2" - const val COMPOSE_BOM_VERSION = "2022.12.00" - const val COMPOSE_COMPILER_VERSION = "1.4.3" + const val COMPOSE_BOM_VERSION = "2023.04.01" + const val COMPOSE_COMPILER_VERSION = "1.4.7" const val COMPOSE_NAVIGATION_VERSION = "2.5.3" const val HILT_VERSION = "2.44" @@ -27,8 +26,8 @@ object Versions { const val JAVAX_INJECT_VERSION = "1" - const val KOTLIN_VERSION = "1.8.10" - const val KOTLINX_COROUTINES_VERSION = "1.6.4" + const val KOTLIN_VERSION = "1.8.21" + const val KOTLINX_COROUTINES_VERSION = "1.7.1" const val KOVER_VERSION = "0.6.0" const val MOSHI_VERSION = "1.12.0" @@ -43,10 +42,11 @@ object Versions { const val DETEKT_VERSION = "1.21.0" // Testing libraries + const val TEST_ANDROIDX_CORE_VERSION = "1.4.0" const val TEST_JUNIT_VERSION = "4.13.2" - const val TEST_KOTEST_VERSION = "5.5.4" - const val TEST_MOCKK_VERSION = "1.12.3" - const val TEST_ROBOLECTRIC_VERSION = "4.9.2" + const val TEST_KOTEST_VERSION = "5.6.2" + const val TEST_MOCKK_VERSION = "1.13.5" + const val TEST_ROBOLECTRIC_VERSION = "4.10.2" const val TEST_RULES_VERSION = "1.5.0" - const val TEST_TURBINE_VERSION = "0.12.1" + const val TEST_TURBINE_VERSION = "0.13.0" } diff --git a/sample-compose/data/build.gradle.kts b/sample-compose/data/build.gradle.kts index 591c4beb0..773ce3e3c 100644 --- a/sample-compose/data/build.gradle.kts +++ b/sample-compose/data/build.gradle.kts @@ -11,7 +11,6 @@ android { minSdk = Versions.ANDROID_MIN_SDK_VERSION targetSdk = Versions.ANDROID_TARGET_SDK_VERSION - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } @@ -81,7 +80,7 @@ dependencies { testImplementation("io.mockk:mockk:${Versions.TEST_MOCKK_VERSION}") testImplementation("io.kotest:kotest-assertions-core:${Versions.TEST_KOTEST_VERSION}") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.KOTLINX_COROUTINES_VERSION}") - testImplementation("androidx.test:core:${Versions.ANDROIDX_TEST_CORE_VERSION}") + testImplementation("androidx.test:core:${Versions.TEST_ANDROIDX_CORE_VERSION}") testImplementation("org.robolectric:robolectric:${Versions.TEST_ROBOLECTRIC_VERSION}") testImplementation("app.cash.turbine:turbine:${Versions.TEST_TURBINE_VERSION}") } diff --git a/template-compose/app/src/test/java/co/nimblehq/template/compose/test/CoroutineTestRule.kt b/template-compose/app/src/test/java/co/nimblehq/template/compose/test/CoroutineTestRule.kt index 3158ba3de..5e9ba791d 100644 --- a/template-compose/app/src/test/java/co/nimblehq/template/compose/test/CoroutineTestRule.kt +++ b/template-compose/app/src/test/java/co/nimblehq/template/compose/test/CoroutineTestRule.kt @@ -8,7 +8,7 @@ import org.junit.runner.Description @OptIn(ExperimentalCoroutinesApi::class) class CoroutineTestRule( - private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() + var testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { val testDispatcherProvider = object : DispatchersProvider { diff --git a/template-compose/app/src/test/java/co/nimblehq/template/compose/ui/screens/BaseScreenTest.kt b/template-compose/app/src/test/java/co/nimblehq/template/compose/ui/screens/BaseScreenTest.kt new file mode 100644 index 000000000..b72b9a43a --- /dev/null +++ b/template-compose/app/src/test/java/co/nimblehq/template/compose/ui/screens/BaseScreenTest.kt @@ -0,0 +1,17 @@ +package co.nimblehq.template.compose.ui.screens + +import co.nimblehq.template.compose.test.CoroutineTestRule +import kotlinx.coroutines.test.StandardTestDispatcher + +abstract class BaseScreenTest { + + protected val coroutinesRule = CoroutineTestRule() + + protected fun setStandardTestDispatcher() { + coroutinesRule.testDispatcher = StandardTestDispatcher() + } + + protected fun advanceUntilIdle() { + coroutinesRule.testDispatcher.scheduler.advanceUntilIdle() + } +} diff --git a/template-compose/app/src/test/java/co/nimblehq/template/compose/ui/screens/home/HomeScreenTest.kt b/template-compose/app/src/test/java/co/nimblehq/template/compose/ui/screens/home/HomeScreenTest.kt index 1ae32c2a9..71a0589af 100644 --- a/template-compose/app/src/test/java/co/nimblehq/template/compose/ui/screens/home/HomeScreenTest.kt +++ b/template-compose/app/src/test/java/co/nimblehq/template/compose/ui/screens/home/HomeScreenTest.kt @@ -5,24 +5,39 @@ import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.* import androidx.test.ext.junit.rules.ActivityScenarioRule import co.nimblehq.template.compose.R +import co.nimblehq.template.compose.domain.model.Model +import co.nimblehq.template.compose.domain.usecase.UseCase import co.nimblehq.template.compose.ui.AppDestination +import co.nimblehq.template.compose.ui.screens.BaseScreenTest import co.nimblehq.template.compose.ui.screens.MainActivity import co.nimblehq.template.compose.ui.theme.ComposeTheme +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.* import org.junit.* import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowToast @RunWith(RobolectricTestRunner::class) -class HomeScreenTest { +class HomeScreenTest : BaseScreenTest() { @get:Rule val composeRule = createAndroidComposeRule() + private val mockUseCase: UseCase = mockk() + + private lateinit var viewModel: HomeViewModel private var expectedAppDestination: AppDestination? = null @Before fun setUp() { - // TODO more setup logic here + every { mockUseCase() } returns flowOf( + listOf(Model(1), Model(2), Model(3)) + ) } @Test @@ -30,16 +45,41 @@ class HomeScreenTest { onNodeWithText(activity.getString(R.string.app_name)).assertIsDisplayed() } + @Test + fun `When entering the Home screen and loading the data failure, it shows the corresponding error`() { + setStandardTestDispatcher() + + val error = Exception() + every { mockUseCase() } returns flow { throw error } + + initComposable { + composeRule.waitForIdle() + advanceUntilIdle() + + ShadowToast.showedToast(activity.getString(R.string.error_generic)) shouldBe true + } + } + private fun initComposable( - testBody: AndroidComposeTestRule, MainActivity>.() -> Unit + testBody: AndroidComposeTestRule, MainActivity>.() -> Unit, ) { + initViewModel() + composeRule.activity.setContent { ComposeTheme { HomeScreen( + viewModel = viewModel, navigator = { destination -> expectedAppDestination = destination } ) } } testBody(composeRule) } + + private fun initViewModel() { + viewModel = HomeViewModel( + coroutinesRule.testDispatcherProvider, + mockUseCase, + ) + } } diff --git a/template-compose/buildSrc/src/main/java/Versions.kt b/template-compose/buildSrc/src/main/java/Versions.kt index 8077a8a74..f3caa22b1 100644 --- a/template-compose/buildSrc/src/main/java/Versions.kt +++ b/template-compose/buildSrc/src/main/java/Versions.kt @@ -9,17 +9,16 @@ object Versions { const val ANDROID_VERSION_NAME = "1.0.0" // Dependencies (Alphabet sorted) - const val ACCOMPANIST_PERMISSIONS_VERSION = "0.28.0" + const val ACCOMPANIST_PERMISSIONS_VERSION = "0.30.1" const val ANDROID_COMMON_KTX_VERSION = "0.1.1" const val ANDROID_CRYPTO_VERSION = "1.0.0" - const val ANDROIDX_CORE_KTX_VERSION = "1.9.0" + const val ANDROIDX_CORE_KTX_VERSION = "1.10.1" const val ANDROIDX_DATASTORE_PREFERENCES_VERSION = "1.0.0" - const val ANDROIDX_LIFECYCLE_VERSION = "2.6.0-rc01" - const val ANDROIDX_TEST_CORE_VERSION = "1.4.0" + const val ANDROIDX_LIFECYCLE_VERSION = "2.6.1" const val CHUCKER_VERSION = "3.5.2" - const val COMPOSE_BOM_VERSION = "2022.12.00" - const val COMPOSE_COMPILER_VERSION = "1.4.3" + const val COMPOSE_BOM_VERSION = "2023.04.01" + const val COMPOSE_COMPILER_VERSION = "1.4.7" const val COMPOSE_NAVIGATION_VERSION = "2.5.3" const val HILT_VERSION = "2.44" @@ -27,8 +26,8 @@ object Versions { const val JAVAX_INJECT_VERSION = "1" - const val KOTLIN_VERSION = "1.8.10" - const val KOTLINX_COROUTINES_VERSION = "1.6.4" + const val KOTLIN_VERSION = "1.8.21" + const val KOTLINX_COROUTINES_VERSION = "1.7.1" const val KOVER_VERSION = "0.6.0" const val MOSHI_VERSION = "1.12.0" @@ -43,9 +42,10 @@ object Versions { const val DETEKT_VERSION = "1.21.0" // Testing libraries + const val TEST_ANDROIDX_CORE_VERSION = "1.4.0" const val TEST_JUNIT_VERSION = "4.13.2" - const val TEST_KOTEST_VERSION = "4.6.3" - const val TEST_MOCKK_VERSION = "1.13.3" - const val TEST_ROBOLECTRIC_VERSION = "4.9.2" - const val TEST_TURBINE_VERSION = "0.12.1" + const val TEST_KOTEST_VERSION = "5.6.2" + const val TEST_MOCKK_VERSION = "1.13.5" + const val TEST_ROBOLECTRIC_VERSION = "4.10.2" + const val TEST_TURBINE_VERSION = "0.13.0" } diff --git a/template-compose/data/build.gradle.kts b/template-compose/data/build.gradle.kts index 591c4beb0..773ce3e3c 100644 --- a/template-compose/data/build.gradle.kts +++ b/template-compose/data/build.gradle.kts @@ -11,7 +11,6 @@ android { minSdk = Versions.ANDROID_MIN_SDK_VERSION targetSdk = Versions.ANDROID_TARGET_SDK_VERSION - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } @@ -81,7 +80,7 @@ dependencies { testImplementation("io.mockk:mockk:${Versions.TEST_MOCKK_VERSION}") testImplementation("io.kotest:kotest-assertions-core:${Versions.TEST_KOTEST_VERSION}") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.KOTLINX_COROUTINES_VERSION}") - testImplementation("androidx.test:core:${Versions.ANDROIDX_TEST_CORE_VERSION}") + testImplementation("androidx.test:core:${Versions.TEST_ANDROIDX_CORE_VERSION}") testImplementation("org.robolectric:robolectric:${Versions.TEST_ROBOLECTRIC_VERSION}") testImplementation("app.cash.turbine:turbine:${Versions.TEST_TURBINE_VERSION}") }