From f109d5c1bb96d6b64506e652367201c742f2ebc5 Mon Sep 17 00:00:00 2001 From: Neouul Date: Tue, 30 Dec 2025 16:22:09 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor:=20UseCase=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SearchToIngredientIntegrationTest.kt | 1 - .../core/di/UseCaseModule.kt | 6 ++++- .../core/di/ViewModelModule.kt | 8 +++---- .../domain/usecase/GetRecipeDetailsUseCase.kt | 23 +++++------------- .../usecase/GetRecipeProcedureUseCase.kt | 24 +++++++++++++++++++ .../domain/usecase/GetRecipesUseCase.kt | 13 ++++++++++ .../presentation/screen/home/HomeViewModel.kt | 7 +++--- .../screen/ingredient/IngredientViewModel.kt | 6 ++--- .../saved_recipe/SavedRecipesViewModel.kt | 8 +++---- .../search_recipe/SearchRecipesViewModel.kt | 7 +++--- 10 files changed, 67 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipeProcedureUseCase.kt create mode 100644 app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipesUseCase.kt diff --git a/app/src/androidTest/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchToIngredientIntegrationTest.kt b/app/src/androidTest/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchToIngredientIntegrationTest.kt index 4c77b085a..10999f660 100644 --- a/app/src/androidTest/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchToIngredientIntegrationTest.kt +++ b/app/src/androidTest/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchToIngredientIntegrationTest.kt @@ -71,7 +71,6 @@ class SearchToIngredientIntegrationTest { override fun copyText(text: String) {} } } - single { GetRecipeProcedureUseCase(get()) } single { CopyLinkUseCase(get()) } viewModel { SearchRecipesViewModel(get()) } viewModel { IngredientViewModel(get(), get(), get()) } diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/UseCaseModule.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/UseCaseModule.kt index d87063621..19e530abb 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/UseCaseModule.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/UseCaseModule.kt @@ -1,12 +1,16 @@ package com.survivalcoding.gangnam2kiandroidstudy.core.di import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.CopyLinkUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeDetailsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeProcedureUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipesUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetSavedRecipesUseCase import org.koin.dsl.module val useCaseModule = module { - single { GetRecipeProcedureUseCase(get()) } single { GetSavedRecipesUseCase(get(), get()) } single { CopyLinkUseCase(get()) } + single { GetRecipesUseCase(get()) } + single { GetRecipeDetailsUseCase(get()) } + single { GetRecipeProcedureUseCase(get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/ViewModelModule.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/ViewModelModule.kt index 4e14bb657..db07a79d5 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/ViewModelModule.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/core/di/ViewModelModule.kt @@ -9,21 +9,21 @@ import org.koin.core.module.dsl.viewModel import org.koin.dsl.module val viewModelModule = module { - viewModel { HomeViewModel(recipeRepository = get()) } + viewModel { HomeViewModel(getRecipesUseCase = get()) } viewModel { SavedRecipesViewModel( - recipeRepository = get(), getSavedRecipesUseCase = get(), + getRecipeDetailsUseCase = get(), ) } viewModel { SearchRecipesViewModel( - recipeRepository = get(), + getRecipesUseCase = get(), ) } viewModel { IngredientViewModel( - recipeRepository = get(), + getRecipeDetailsUseCase = get(), getRecipeProcedureUseCase = get(), copyLinkUseCase = get(), ) diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipeDetailsUseCase.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipeDetailsUseCase.kt index 71b8b240d..2ef694152 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipeDetailsUseCase.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipeDetailsUseCase.kt @@ -1,24 +1,13 @@ package com.survivalcoding.gangnam2kiandroidstudy.domain.usecase import com.survivalcoding.gangnam2kiandroidstudy.core.Result -import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.ProcedureRepository +import com.survivalcoding.gangnam2kiandroidstudy.domain.model.Recipe +import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.RecipeRepository -class GetRecipeProcedureUseCase( - private val procedureRepository: ProcedureRepository, +class GetRecipeDetailsUseCase( + private val recipeRepository: RecipeRepository ) { - suspend fun execute(recipeId: Long): Result, String> { - try { - val result = procedureRepository.getProcedure(recipeId) - return Result.Success( - listOf( - "Lorem Ipsum tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur?", - "Lorem Ipsum tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur? Tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur?", - "Lorem Ipsum tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur?", - "Lorem Ipsum tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur?", - ) - ) - } catch (e: Exception) { - return Result.Error("Failed to get recipe's procedure: $e") - } + suspend fun execute(id: Long): Result { + return recipeRepository.findRecipe(id) } } \ No newline at end of file diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipeProcedureUseCase.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipeProcedureUseCase.kt new file mode 100644 index 000000000..b1bffd61b --- /dev/null +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipeProcedureUseCase.kt @@ -0,0 +1,24 @@ +package com.survivalcoding.gangnam2kiandroidstudy.domain.usecase + +import com.survivalcoding.gangnam2kiandroidstudy.core.Result +import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.ProcedureRepository + +class GetRecipeProcedureUseCase( + private val procedureRepository: ProcedureRepository, +) { + suspend fun execute(recipeId: Long): Result, String> { + try { + val result = procedureRepository.getProcedure(recipeId) + return Result.Success( + listOf( + "Lorem Ipsum tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur?", + "Lorem Ipsum tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur? Tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur?", + "Lorem Ipsum tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur?", + "Lorem Ipsum tempor incididunt ut labore et dolore,in voluptate velit esse cillum dolore eu fugiat nulla pariatur?", + ) + ) + } catch (e: Exception) { + return Result.Error("Failed to get recipe's procedure: $e") + } + } +} diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipesUseCase.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipesUseCase.kt new file mode 100644 index 000000000..255800c13 --- /dev/null +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/domain/usecase/GetRecipesUseCase.kt @@ -0,0 +1,13 @@ +package com.survivalcoding.gangnam2kiandroidstudy.domain.usecase + +import com.survivalcoding.gangnam2kiandroidstudy.core.Result +import com.survivalcoding.gangnam2kiandroidstudy.domain.model.Recipe +import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.RecipeRepository + +class GetRecipesUseCase( + private val recipeRepository: RecipeRepository +) { + suspend fun execute(): Result, String> { + return recipeRepository.findRecipes() + } +} diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeViewModel.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeViewModel.kt index 895bab323..57019878e 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeViewModel.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/home/HomeViewModel.kt @@ -5,7 +5,8 @@ import androidx.lifecycle.viewModelScope import com.survivalcoding.gangnam2kiandroidstudy.core.Result import com.survivalcoding.gangnam2kiandroidstudy.domain.model.RecipeCategory import com.survivalcoding.gangnam2kiandroidstudy.domain.model.toCategory -import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.RecipeRepository +import com.survivalcoding.gangnam2kiandroidstudy.domain.model.toCategory +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipesUseCase import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -16,7 +17,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class HomeViewModel( - private val recipeRepository: RecipeRepository, + private val getRecipesUseCase: GetRecipesUseCase, ) : ViewModel() { private val _state = MutableStateFlow(HomeState()) @@ -98,7 +99,7 @@ class HomeViewModel( loadJob = viewModelScope.launch { _state.update { it.copy(isLoading = true, isError = false) } - when (val response = recipeRepository.findRecipes()) { + when (val response = getRecipesUseCase.execute()) { is Result.Success -> _state.update { currentState -> val all = response.data diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientViewModel.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientViewModel.kt index b122ec709..082152dcc 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientViewModel.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/ingredient/IngredientViewModel.kt @@ -3,8 +3,8 @@ package com.survivalcoding.gangnam2kiandroidstudy.presentation.screen.ingredient import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.survivalcoding.gangnam2kiandroidstudy.core.Result -import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.RecipeRepository import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.CopyLinkUseCase +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeDetailsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeProcedureUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class IngredientViewModel( - private val recipeRepository: RecipeRepository, + private val getRecipeDetailsUseCase: GetRecipeDetailsUseCase, private val getRecipeProcedureUseCase: GetRecipeProcedureUseCase, private val copyLinkUseCase: CopyLinkUseCase, ) : ViewModel() { @@ -27,7 +27,7 @@ class IngredientViewModel( fun loadRecipeDetail(recipeId: Long) { viewModelScope.launch { - when (val response = recipeRepository.findRecipe(recipeId)) { + when (val response = getRecipeDetailsUseCase.execute(recipeId)) { is Result.Success -> _state.update { it.copy(recipe = response.data) } is Result.Error -> println("에러 처리") diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModel.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModel.kt index 74ba87c99..198c162ca 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModel.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModel.kt @@ -3,7 +3,7 @@ package com.survivalcoding.gangnam2kiandroidstudy.presentation.screen.saved_reci import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.survivalcoding.gangnam2kiandroidstudy.core.Result -import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.RecipeRepository +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeDetailsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetSavedRecipesUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -12,8 +12,8 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class SavedRecipesViewModel( - private val recipeRepository: RecipeRepository, private val getSavedRecipesUseCase: GetSavedRecipesUseCase, + private val getRecipeDetailsUseCase: GetRecipeDetailsUseCase, ) : ViewModel() { private val _state = MutableStateFlow(SavedRecipesState()) @@ -27,7 +27,7 @@ class SavedRecipesViewModel( } suspend fun loadRecipes() { - when (val response = recipeRepository.findRecipes()) { + when (val response = getSavedRecipesUseCase.execute()) { is Result.Success -> _state.update { it.copy(savedRecipes = response.data) } @@ -37,7 +37,7 @@ class SavedRecipesViewModel( } suspend fun saveNewRecipe(id: Long) { - when (val response = recipeRepository.findRecipe(id)) { + when (val response = getRecipeDetailsUseCase.execute(id)) { is Result.Success -> _state.update { currentState -> // 이미 저장된 레시피면 그대로 if (currentState.savedRecipes.any { it.id == id }) { diff --git a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchRecipesViewModel.kt b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchRecipesViewModel.kt index efb81c526..b06ae2c78 100644 --- a/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchRecipesViewModel.kt +++ b/app/src/main/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchRecipesViewModel.kt @@ -5,7 +5,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.survivalcoding.gangnam2kiandroidstudy.core.Result import com.survivalcoding.gangnam2kiandroidstudy.domain.model.toFormatString -import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.RecipeRepository +import com.survivalcoding.gangnam2kiandroidstudy.domain.model.toFormatString +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipesUseCase import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -20,7 +21,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch open class SearchRecipesViewModel( - private val recipeRepository: RecipeRepository, + private val getRecipesUseCase: GetRecipesUseCase, ) : ViewModel() { // 상태 @@ -165,7 +166,7 @@ open class SearchRecipesViewModel( viewModelScope.launch { _state.update { it.copy(isLoading = true) } - when (val response = recipeRepository.findRecipes()) { + when (val response = getRecipesUseCase.execute()) { is Result.Success -> { _state.update { currentState -> currentState.copy( From 13f5614bd3615fedb4723d743cb750571672b33e Mon Sep 17 00:00:00 2001 From: Neouul Date: Tue, 30 Dec 2025 16:36:41 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20UseCase=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=82=AC=ED=95=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SavedRecipes, SearchRecipes 테스트의 모킹 로직을 Repository에서 UseCase로 변경하여 의존성을 개선 --- .../saved_recipe/SavedRecipesViewModelTest.kt | 77 ++++++++++--------- .../SearchRecipesViewModelTest.kt | 28 ++++--- 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/app/src/test/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModelTest.kt b/app/src/test/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModelTest.kt index af28f00ee..9e4b325de 100644 --- a/app/src/test/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModelTest.kt +++ b/app/src/test/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/saved_recipe/SavedRecipesViewModelTest.kt @@ -1,35 +1,30 @@ package com.survivalcoding.gangnam2kiandroidstudy.presentation.screen.saved_recipe -import androidx.lifecycle.SavedStateHandle -import com.survivalcoding.gangnam2kiandroidstudy.data.data_source.MockRecipeDataSourceImpl -import com.survivalcoding.gangnam2kiandroidstudy.data.data_source.RecipeDataSource -import com.survivalcoding.gangnam2kiandroidstudy.data.mapper.toModel -import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.RecipeRepository -import com.survivalcoding.gangnam2kiandroidstudy.data.repository.RecipeRepositoryImpl import com.survivalcoding.gangnam2kiandroidstudy.core.Result -import com.survivalcoding.gangnam2kiandroidstudy.domain.model.Ingredient import com.survivalcoding.gangnam2kiandroidstudy.domain.model.Recipe import com.survivalcoding.gangnam2kiandroidstudy.domain.model.RecipeCategory +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipeDetailsUseCase import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetSavedRecipesUseCase import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class SavedRecipesViewModelTest { private val testDispatcher = StandardTestDispatcher() - private val dataSource: RecipeDataSource = MockRecipeDataSourceImpl() - private val repository: RecipeRepository = RecipeRepositoryImpl(dataSource) // 가짜 리스트 + private val getSavedRecipesUseCase: GetSavedRecipesUseCase = mockk() + private val getRecipeDetailsUseCase: GetRecipeDetailsUseCase = mockk() @Before fun setup() { @@ -41,32 +36,45 @@ class SavedRecipesViewModelTest { Dispatchers.resetMain() } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun `loadRecipes 메서드 - 모든 레시피를 받아온다`() = runTest { // given - val viewModel: SavedRecipesViewModel = SavedRecipesViewModel( - recipeRepository = repository, - getSavedRecipesUseCase = mockk(), + val sampleRecipes = listOf( + Recipe( + id = 1, + category = RecipeCategory.INDIAN, + name = "Traditional Indian Curry", + imageUrl = "", + chef = "Chef A", + time = "30 mins", + rating = 4.5, + ingredients = emptyList() + ) + ) + coEvery { getSavedRecipesUseCase.execute() } returns Result.Success(sampleRecipes) + + val viewModel = SavedRecipesViewModel( + getSavedRecipesUseCase = getSavedRecipesUseCase, + getRecipeDetailsUseCase = getRecipeDetailsUseCase ) // when - val expected = dataSource.getRecipes()?.filterNotNull()?.map { it.toModel() } ?: emptyList() - viewModel.loadRecipes() - // launch가 Main 디스패처에서 돌기 때문에 기다려줘야 함 - advanceUntilIdle() + // init block calls loadRecipes() which launches a coroutine. + // We need to advance time to let it execute. + testDispatcher.scheduler.advanceUntilIdle() // then assertTrue(viewModel.state.value.savedRecipes.isNotEmpty()) - assertEquals(expected, viewModel.state.value.savedRecipes) + assertEquals(sampleRecipes, viewModel.state.value.savedRecipes) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun `saveNewRecipe 메서드 - 특정 id의 레시피를 state에 추가한다`() = runTest { - // given: ViewModel의 init에서 모든 레시피를 불러오도록 되어있음 - // 특정 레시피 저장 확인을 위해 모킹 - val expected = Recipe( + // given + // Initial load returns empty list + coEvery { getSavedRecipesUseCase.execute() } returns Result.Success(emptyList()) + + val newRecipe = Recipe( id = 2, category = RecipeCategory.ASIAN, name = "Spice roasted chicken with flavored rice", @@ -76,25 +84,20 @@ class SavedRecipesViewModelTest { rating = 4.0, ingredients = listOf() ) + coEvery { getRecipeDetailsUseCase.execute(2) } returns Result.Success(newRecipe) - val mockRepository = mockk() - coEvery { mockRepository.findRecipes() } returns Result.Success(listOf()) - // findRecipe()도 반드시 mock 해야 함! - coEvery { mockRepository.findRecipe(2) } returns Result.Success(expected) - - val viewModel: SavedRecipesViewModel = SavedRecipesViewModel( - recipeRepository = mockRepository, - getSavedRecipesUseCase = mockk(), + val viewModel = SavedRecipesViewModel( + getSavedRecipesUseCase = getSavedRecipesUseCase, + getRecipeDetailsUseCase = getRecipeDetailsUseCase ) - advanceUntilIdle() + testDispatcher.scheduler.advanceUntilIdle() // Finish init load // when: id가 2인 레시피 추가하기 viewModel.saveNewRecipe(2) - advanceUntilIdle() + testDispatcher.scheduler.advanceUntilIdle() // Finish saveNewRecipe // then assertTrue(viewModel.state.value.savedRecipes.isNotEmpty()) - assertEquals(listOf(expected), viewModel.state.value.savedRecipes) + assertEquals(listOf(newRecipe), viewModel.state.value.savedRecipes) } - -} \ No newline at end of file +} diff --git a/app/src/test/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchRecipesViewModelTest.kt b/app/src/test/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchRecipesViewModelTest.kt index 03ac66ab8..813f46dc6 100644 --- a/app/src/test/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchRecipesViewModelTest.kt +++ b/app/src/test/java/com/survivalcoding/gangnam2kiandroidstudy/presentation/screen/search_recipe/SearchRecipesViewModelTest.kt @@ -4,7 +4,7 @@ import com.survivalcoding.gangnam2kiandroidstudy.core.Result import com.survivalcoding.gangnam2kiandroidstudy.domain.model.Recipe import com.survivalcoding.gangnam2kiandroidstudy.domain.model.RecipeCategory import com.survivalcoding.gangnam2kiandroidstudy.domain.model.toFormatString -import com.survivalcoding.gangnam2kiandroidstudy.domain.repository.RecipeRepository +import com.survivalcoding.gangnam2kiandroidstudy.domain.usecase.GetRecipesUseCase import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.Dispatchers @@ -20,7 +20,6 @@ import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -29,7 +28,7 @@ import org.junit.Test class SearchRecipesViewModelTest { private lateinit var viewModel: SearchRecipesViewModel - private val recipeRepository: RecipeRepository = mockk() + private val getRecipesUseCase: GetRecipesUseCase = mockk() private val testDispatcher = StandardTestDispatcher() private val sampleRecipes = listOf( @@ -68,8 +67,8 @@ class SearchRecipesViewModelTest { @Before fun setUp() { Dispatchers.setMain(testDispatcher) - coEvery { recipeRepository.findRecipes() } returns Result.Success(sampleRecipes) - viewModel = SearchRecipesViewModel(recipeRepository) + coEvery { getRecipesUseCase.execute() } returns Result.Success(sampleRecipes) + viewModel = SearchRecipesViewModel(getRecipesUseCase) } @After @@ -80,11 +79,11 @@ class SearchRecipesViewModelTest { @Test fun `Initial State Validation`() = runTest { // Override mock to simulate delay for loading state verification - coEvery { recipeRepository.findRecipes() } coAnswers { + coEvery { getRecipesUseCase.execute() } coAnswers { delay(100) Result.Success(emptyList()) } - val initialStateViewModel = SearchRecipesViewModel(recipeRepository) + val initialStateViewModel = SearchRecipesViewModel(getRecipesUseCase) // Before advancing, it should be in default state (isLoading=false) assertFalse(initialStateViewModel.state.value.isLoading) @@ -101,7 +100,6 @@ class SearchRecipesViewModelTest { } @Test - fun `Successful Recipe Loading`() = runTest { advanceTimeBy(1) // let init's loadRecipes complete testDispatcher.scheduler.advanceUntilIdle() @@ -112,8 +110,8 @@ class SearchRecipesViewModelTest { @Test fun `Failed Recipe Loading`() = runTest { - coEvery { recipeRepository.findRecipes() } returns Result.Error("Error") - val failViewModel = SearchRecipesViewModel(recipeRepository) + coEvery { getRecipesUseCase.execute() } returns Result.Error("Error") + val failViewModel = SearchRecipesViewModel(getRecipesUseCase) testDispatcher.scheduler.advanceUntilIdle() @@ -123,8 +121,8 @@ class SearchRecipesViewModelTest { @Test fun `Empty Recipe List from Repository`() = runTest { - coEvery { recipeRepository.findRecipes() } returns Result.Success(emptyList()) - val emptyViewModel = SearchRecipesViewModel(recipeRepository) + coEvery { getRecipesUseCase.execute() } returns Result.Success(emptyList()) + val emptyViewModel = SearchRecipesViewModel(getRecipesUseCase) testDispatcher.scheduler.advanceUntilIdle() @@ -476,14 +474,14 @@ class SearchRecipesViewModelTest { // No, protected is only for subclasses. // However, we can use a subclass for testing if needed. - class TestViewModel(repo: RecipeRepository) : SearchRecipesViewModel(repo) { + class TestViewModel(useCase: GetRecipesUseCase) : SearchRecipesViewModel(useCase) { public override fun onCleared() { super.onCleared() } } - val testViewModel = TestViewModel(recipeRepository) + val testViewModel = TestViewModel(getRecipesUseCase) testViewModel.onCleared() // No crash } -} \ No newline at end of file +}