Add ViewModel tests

This commit is contained in:
Kirill Kamakin
2022-12-16 21:19:19 +01:00
parent 9a3a30aca2
commit c1a292851a
4 changed files with 57 additions and 15 deletions

View File

@@ -72,6 +72,7 @@ class RecipesListViewModel @Inject constructor(
logger.v { "onDeleteConfirm() called with: recipeSummaryEntity = $recipeSummaryEntity" } logger.v { "onDeleteConfirm() called with: recipeSummaryEntity = $recipeSummaryEntity" }
viewModelScope.launch { viewModelScope.launch {
val result = recipeRepo.deleteRecipe(recipeSummaryEntity) val result = recipeRepo.deleteRecipe(recipeSummaryEntity)
logger.d { "onDeleteConfirm: delete result is $result" }
_deleteRecipeResult.emit(result) _deleteRecipeResult.emit(result)
} }
} }

View File

@@ -5,15 +5,23 @@ import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.test.BaseUnitTest import gq.kirmanak.mealient.test.BaseUnitTest
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf 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 kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test import org.junit.Test
import java.io.IOException
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class RecipesListViewModelTest : BaseUnitTest() { class RecipesListViewModelTest : BaseUnitTest() {
@@ -24,6 +32,12 @@ class RecipesListViewModelTest : BaseUnitTest() {
@MockK(relaxed = true) @MockK(relaxed = true)
lateinit var recipeRepo: RecipeRepo lateinit var recipeRepo: RecipeRepo
@Before
override fun setUp() {
super.setUp()
every { authRepo.isAuthorizedFlow } returns flowOf(true)
}
@Test @Test
fun `when authRepo isAuthorized changes to true expect that recipes are refreshed`() { fun `when authRepo isAuthorized changes to true expect that recipes are refreshed`() {
every { authRepo.isAuthorizedFlow } returns flowOf(false, true) every { authRepo.isAuthorizedFlow } returns flowOf(false, true)
@@ -40,14 +54,12 @@ class RecipesListViewModelTest : BaseUnitTest() {
@Test @Test
fun `when authRepo isAuthorized doesn't change expect that recipes are not refreshed`() { fun `when authRepo isAuthorized doesn't change expect that recipes are not refreshed`() {
every { authRepo.isAuthorizedFlow } returns flowOf(true)
createSubject() createSubject()
coVerify(inverse = true) { recipeRepo.refreshRecipes() } coVerify(inverse = true) { recipeRepo.refreshRecipes() }
} }
@Test @Test
fun `when refreshRecipeInfo succeeds expect successful result`() = runTest { fun `when refreshRecipeInfo succeeds expect successful result`() = runTest {
every { authRepo.isAuthorizedFlow } returns flowOf(true)
val slug = "cake" val slug = "cake"
coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit) coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit)
val actual = createSubject().refreshRecipeInfo(slug).asFlow().first() val actual = createSubject().refreshRecipeInfo(slug).asFlow().first()
@@ -56,7 +68,6 @@ class RecipesListViewModelTest : BaseUnitTest() {
@Test @Test
fun `when refreshRecipeInfo succeeds expect call to repo`() = runTest { fun `when refreshRecipeInfo succeeds expect call to repo`() = runTest {
every { authRepo.isAuthorizedFlow } returns flowOf(true)
val slug = "cake" val slug = "cake"
coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit) coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit)
createSubject().refreshRecipeInfo(slug).asFlow().first() createSubject().refreshRecipeInfo(slug).asFlow().first()
@@ -65,7 +76,6 @@ class RecipesListViewModelTest : BaseUnitTest() {
@Test @Test
fun `when refreshRecipeInfo fails expect result with error`() = runTest { fun `when refreshRecipeInfo fails expect result with error`() = runTest {
every { authRepo.isAuthorizedFlow } returns flowOf(true)
val slug = "cake" val slug = "cake"
val result = Result.failure<Unit>(RuntimeException()) val result = Result.failure<Unit>(RuntimeException())
coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns result coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns result
@@ -73,5 +83,38 @@ class RecipesListViewModelTest : BaseUnitTest() {
assertThat(actual).isEqualTo(result) 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 <T> TestScope.runTestAndCollectFlow(
flow: Flow<T>,
block: () -> Unit,
): List<T> {
val results = mutableListOf<T>()
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
flow.toList(results)
}
block()
collectJob.cancel()
return results
}
private fun createSubject() = RecipesListViewModel(recipeRepo, authRepo, logger) private fun createSubject() = RecipesListViewModel(recipeRepo, authRepo, logger)
} }

View File

@@ -15,9 +15,7 @@ import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.Timeout
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class ShareRecipeViewModelTest : BaseUnitTest() { class ShareRecipeViewModelTest : BaseUnitTest() {
@@ -27,9 +25,6 @@ class ShareRecipeViewModelTest : BaseUnitTest() {
lateinit var subject: ShareRecipeViewModel lateinit var subject: ShareRecipeViewModel
@get:Rule
val timeoutRule: Timeout = Timeout.seconds(5)
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()

View File

@@ -7,20 +7,23 @@ import io.mockk.MockKAnnotations
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.rules.Timeout
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
open class BaseUnitTest { open class BaseUnitTest {
@get:Rule @get:Rule(order = 0)
val instantExecutorRule = InstantTaskExecutorRule() val instantExecutorRule = InstantTaskExecutorRule()
@get:Rule(order = 1)
val timeoutRule: Timeout = Timeout.seconds(10)
protected val logger: Logger = FakeLogger() protected val logger: Logger = FakeLogger()
lateinit var dispatchers: AppDispatchers lateinit var dispatchers: AppDispatchers
@@ -30,10 +33,10 @@ open class BaseUnitTest {
MockKAnnotations.init(this) MockKAnnotations.init(this)
Dispatchers.setMain(UnconfinedTestDispatcher()) Dispatchers.setMain(UnconfinedTestDispatcher())
dispatchers = object : AppDispatchers { dispatchers = object : AppDispatchers {
override val io: CoroutineDispatcher = StandardTestDispatcher() override val io: CoroutineDispatcher = UnconfinedTestDispatcher()
override val main: CoroutineDispatcher = StandardTestDispatcher() override val main: CoroutineDispatcher = UnconfinedTestDispatcher()
override val default: CoroutineDispatcher = StandardTestDispatcher() override val default: CoroutineDispatcher = UnconfinedTestDispatcher()
override val unconfined: CoroutineDispatcher = StandardTestDispatcher() override val unconfined: CoroutineDispatcher = UnconfinedTestDispatcher()
} }
} }