diff --git a/build.module.feature-and-app.gradle b/build.module.feature-and-app.gradle index 44389cf..dc26fc7 100644 --- a/build.module.feature-and-app.gradle +++ b/build.module.feature-and-app.gradle @@ -42,4 +42,10 @@ dependencies { implementation(libs.napier) implementation(libs.activityCompose) implementation(libs.hiltNavigationCompose) -} \ No newline at end of file + + testImplementation libs.junit + testImplementation libs.kotlinx.coroutines.test + testImplementation libs.turbine + testImplementation libs.mockk.android + testImplementation libs.mockk.agent +} diff --git a/feature/home/src/test/kotlin/nl/q42/template/presentation/home/HomeViewModelTest.kt b/feature/home/src/test/kotlin/nl/q42/template/presentation/home/HomeViewModelTest.kt new file mode 100644 index 0000000..9bf6438 --- /dev/null +++ b/feature/home/src/test/kotlin/nl/q42/template/presentation/home/HomeViewModelTest.kt @@ -0,0 +1,56 @@ +package nl.q42.template.presentation.home + +import app.cash.turbine.test +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import nl.q42.template.actionresult.domain.ActionResult +import nl.q42.template.domain.user.model.User +import nl.q42.template.domain.user.usecase.GetUserUseCase +import org.junit.Rule +import org.junit.Test +import kotlin.time.Duration.Companion.seconds + + +class HomeViewModelTest(){ + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + @Test + fun `WHEN I subscribe to uiState with a slow UserUseCase THEN I get the loading state and expected email address`() = runTest{ + val getUserUseCaseMock: GetUserUseCase = mockk() + coEvery {getUserUseCaseMock.invoke() }.coAnswers { + // demonstration of test scheduler. This does not actually block the test for 4 seconds + delay(4.seconds) + ActionResult.Success(User("test@test.com")) + } + + val viewModel = HomeViewModel(getUserUseCaseMock, mockk()) + + viewModel.uiState.test { + val expectedData: HomeViewState = HomeViewState.Data("test@test.com") + + assertEquals(HomeViewState.Loading, awaitItem()) + assertEquals(expectedData, awaitItem()) + } + } + + @Test + fun `WHEN I subscribe to uiState with a fast UserUseCase THEN I get expected email address immediately`() = runTest{ + val getUserUseCaseMock: GetUserUseCase = mockk() + coEvery { getUserUseCaseMock.invoke() }.returns( + ActionResult.Success(User("test@test.com") + )) + + + val viewModel = HomeViewModel(getUserUseCaseMock, mockk()) + + viewModel.uiState.test { + val expectedData: HomeViewState = HomeViewState.Data("test@test.com") + assertEquals(expectedData, awaitItem()) + } + } + +} diff --git a/feature/home/src/test/kotlin/nl/q42/template/presentation/home/MainDispatcherRule.kt b/feature/home/src/test/kotlin/nl/q42/template/presentation/home/MainDispatcherRule.kt new file mode 100644 index 0000000..32eef3a --- /dev/null +++ b/feature/home/src/test/kotlin/nl/q42/template/presentation/home/MainDispatcherRule.kt @@ -0,0 +1,22 @@ +package nl.q42.template.presentation.home + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +// Reusable JUnit4 TestRule to override the Main dispatcher +class MainDispatcherRule( + val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), +) : TestWatcher() { + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description) { + Dispatchers.resetMain() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d02cbba..03516da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,9 @@ [versions] androidxCore = "1.9.0" androidxLifecycle = "2.5.1" +kotlinxCoroutinesTest = "1.7.3" +junit = "4.13.2" +mockkAndroid = "1.13.5" kotlinCompilerExtensionVersion = "1.4.4" jvmTarget = "17" kotlin = "1.8.10" @@ -19,12 +22,18 @@ composePlatform = "2022.10.00" activityCompose = "1.6.1" hiltNavigationCompose = "1.0.0" composeLifecycle = "2.6.0-beta01" +turbine = "1.0.0" +appcompat = "1.6.1" [libraries] androidxCoreKtx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } androidxLifecycleKtx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hiltKapt = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +junit = { module = "junit:junit", version.ref = "junit" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } +mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockkAndroid" } +mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockkAndroid" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } moshiRetrofitConverter = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } moshiKotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } @@ -44,6 +53,7 @@ composePlatform = { module = "androidx.compose:compose-bom", version.ref = "comp activityCompose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } hiltNavigationCompose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } composeLifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "composeLifecycle" } +turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } [plugins]