diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt index 7efeaad..f9ddddf 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModel.kt @@ -72,6 +72,7 @@ class RecipesListViewModel @Inject constructor( logger.v { "onDeleteConfirm() called with: recipeSummaryEntity = $recipeSummaryEntity" } viewModelScope.launch { val result = recipeRepo.deleteRecipe(recipeSummaryEntity) + logger.d { "onDeleteConfirm: delete result is $result" } _deleteRecipeResult.emit(result) } } diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModelTest.kt index 3d9ccbb..cb071cd 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/recipes/RecipesListViewModelTest.kt @@ -5,15 +5,23 @@ import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.test.BaseUnitTest +import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test +import java.io.IOException @OptIn(ExperimentalCoroutinesApi::class) class RecipesListViewModelTest : BaseUnitTest() { @@ -24,6 +32,12 @@ class RecipesListViewModelTest : BaseUnitTest() { @MockK(relaxed = true) lateinit var recipeRepo: RecipeRepo + @Before + override fun setUp() { + super.setUp() + every { authRepo.isAuthorizedFlow } returns flowOf(true) + } + @Test fun `when authRepo isAuthorized changes to true expect that recipes are refreshed`() { every { authRepo.isAuthorizedFlow } returns flowOf(false, true) @@ -40,14 +54,12 @@ class RecipesListViewModelTest : BaseUnitTest() { @Test fun `when authRepo isAuthorized doesn't change expect that recipes are not refreshed`() { - every { authRepo.isAuthorizedFlow } returns flowOf(true) createSubject() coVerify(inverse = true) { recipeRepo.refreshRecipes() } } @Test fun `when refreshRecipeInfo succeeds expect successful result`() = runTest { - every { authRepo.isAuthorizedFlow } returns flowOf(true) val slug = "cake" coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit) val actual = createSubject().refreshRecipeInfo(slug).asFlow().first() @@ -56,7 +68,6 @@ class RecipesListViewModelTest : BaseUnitTest() { @Test fun `when refreshRecipeInfo succeeds expect call to repo`() = runTest { - every { authRepo.isAuthorizedFlow } returns flowOf(true) val slug = "cake" coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit) createSubject().refreshRecipeInfo(slug).asFlow().first() @@ -65,7 +76,6 @@ class RecipesListViewModelTest : BaseUnitTest() { @Test fun `when refreshRecipeInfo fails expect result with error`() = runTest { - every { authRepo.isAuthorizedFlow } returns flowOf(true) val slug = "cake" val result = Result.failure(RuntimeException()) coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns result @@ -73,5 +83,38 @@ class RecipesListViewModelTest : BaseUnitTest() { assertThat(actual).isEqualTo(result) } + @Test + fun `when delete recipe expect successful result in flow`() = runTest { + coEvery { recipeRepo.deleteRecipe(any()) } returns Result.success(Unit) + val subject = createSubject() + val results = runTestAndCollectFlow(subject.deleteRecipeResult) { + subject.onDeleteConfirm(CAKE_RECIPE_SUMMARY_ENTITY) + } + assertThat(results.single().isSuccess).isTrue() + } + + @Test + fun `when delete recipe expect failed result in flow`() = runTest { + coEvery { recipeRepo.deleteRecipe(any()) } returns Result.failure(IOException()) + val subject = createSubject() + val results = runTestAndCollectFlow(subject.deleteRecipeResult) { + subject.onDeleteConfirm(CAKE_RECIPE_SUMMARY_ENTITY) + } + assertThat(results.single().isFailure).isTrue() + } + + private inline fun TestScope.runTestAndCollectFlow( + flow: Flow, + block: () -> Unit, + ): List { + val results = mutableListOf() + val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) { + flow.toList(results) + } + block() + collectJob.cancel() + return results + } + private fun createSubject() = RecipesListViewModel(recipeRepo, authRepo, logger) } \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/share/ShareRecipeViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/share/ShareRecipeViewModelTest.kt index 4046062..5767628 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/share/ShareRecipeViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/share/ShareRecipeViewModelTest.kt @@ -15,9 +15,7 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.junit.rules.Timeout @OptIn(ExperimentalCoroutinesApi::class) class ShareRecipeViewModelTest : BaseUnitTest() { @@ -27,9 +25,6 @@ class ShareRecipeViewModelTest : BaseUnitTest() { lateinit var subject: ShareRecipeViewModel - @get:Rule - val timeoutRule: Timeout = Timeout.seconds(5) - @Before override fun setUp() { super.setUp() diff --git a/testing/src/main/kotlin/gq/kirmanak/mealient/test/BaseUnitTest.kt b/testing/src/main/kotlin/gq/kirmanak/mealient/test/BaseUnitTest.kt index aabb40c..50bc7b0 100644 --- a/testing/src/main/kotlin/gq/kirmanak/mealient/test/BaseUnitTest.kt +++ b/testing/src/main/kotlin/gq/kirmanak/mealient/test/BaseUnitTest.kt @@ -7,20 +7,23 @@ import io.mockk.MockKAnnotations import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Rule +import org.junit.rules.Timeout @OptIn(ExperimentalCoroutinesApi::class) open class BaseUnitTest { - @get:Rule + @get:Rule(order = 0) val instantExecutorRule = InstantTaskExecutorRule() + @get:Rule(order = 1) + val timeoutRule: Timeout = Timeout.seconds(10) + protected val logger: Logger = FakeLogger() lateinit var dispatchers: AppDispatchers @@ -30,10 +33,10 @@ open class BaseUnitTest { MockKAnnotations.init(this) Dispatchers.setMain(UnconfinedTestDispatcher()) dispatchers = object : AppDispatchers { - override val io: CoroutineDispatcher = StandardTestDispatcher() - override val main: CoroutineDispatcher = StandardTestDispatcher() - override val default: CoroutineDispatcher = StandardTestDispatcher() - override val unconfined: CoroutineDispatcher = StandardTestDispatcher() + override val io: CoroutineDispatcher = UnconfinedTestDispatcher() + override val main: CoroutineDispatcher = UnconfinedTestDispatcher() + override val default: CoroutineDispatcher = UnconfinedTestDispatcher() + override val unconfined: CoroutineDispatcher = UnconfinedTestDispatcher() } }