From de13ab57fa752d95cf5049ac1ab9b9641e02c211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 20 Sep 2026 22:36:47 +0900 Subject: [PATCH 1/2] fix(setting): propagate repository cancellation (#1501) --- .../account/ConnectedAccountsScreen.kt | 2 +- .../account/ConnectedAccountsViewModel.kt | 9 +- .../presentation/home/SettingViewModel.kt | 5 +- .../home/WithdrawConfirmScreen.kt | 2 +- .../notification/NotificationSettingScreen.kt | 2 +- .../notification/PushNotificationScreen.kt | 2 +- .../notification/PushNotificationViewModel.kt | 4 +- .../SettingCoroutineCancellationTest.kt | 368 ++++++++++++++++++ 8 files changed, 382 insertions(+), 12 deletions(-) create mode 100644 feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreen.kt index 38eafcf3a..27398d8cb 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsScreen.kt @@ -22,7 +22,7 @@ import com.afternote.feature.setting.presentation.account.social.requestGoogleId import com.afternote.feature.setting.presentation.account.social.toKakaoAuthResult @Composable -fun ConnectedAccountsScreen( +internal fun ConnectedAccountsScreen( onBack: () -> Unit, modifier: Modifier = Modifier, viewModel: ConnectedAccountsViewModel = hiltViewModel(), diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsViewModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsViewModel.kt index 9bb399211..88d50b126 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsViewModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/account/ConnectedAccountsViewModel.kt @@ -2,6 +2,7 @@ package com.afternote.feature.setting.presentation.account import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.afternote.core.common.result.runCatchingCancellable import com.afternote.core.model.user.UserConnectedAccount import com.afternote.feature.setting.domain.SettingAccountRepository import com.afternote.feature.setting.presentation.R @@ -15,7 +16,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class ConnectedAccountsViewModel +internal class ConnectedAccountsViewModel @Inject constructor( private val accountRepository: SettingAccountRepository, @@ -32,7 +33,7 @@ class ConnectedAccountsViewModel private fun loadConnectedAccounts() { viewModelScope.launch { - runCatching { accountRepository.getConnectedAccounts() } + runCatchingCancellable { accountRepository.getConnectedAccounts() } .onSuccess { accounts -> _uiState.update { it.copy(isLoading = false, accounts = accounts.toStateList()) } }.onFailure { @@ -61,7 +62,7 @@ class ConnectedAccountsViewModel accessToken: String, ) { viewModelScope.launch { - runCatching { accountRepository.linkConnectedAccount(provider, accessToken) } + runCatchingCancellable { accountRepository.linkConnectedAccount(provider, accessToken) } .onSuccess { accounts -> _uiState.update { it.copy(accounts = accounts.toStateList()) } } .onFailure { _uiState.update { it.copy(errorMessage = "계정 연결에 실패했습니다.") } } } @@ -69,7 +70,7 @@ class ConnectedAccountsViewModel private fun unlink(provider: String) { viewModelScope.launch { - runCatching { accountRepository.unlinkConnectedAccount(provider) } + runCatchingCancellable { accountRepository.unlinkConnectedAccount(provider) } .onSuccess { accounts -> _uiState.update { it.copy(accounts = accounts.toStateList()) } } .onFailure { _uiState.update { it.copy(errorMessage = "계정 연결 해제에 실패했습니다.") } } } diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModel.kt index d051696f5..e28c05820 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/SettingViewModel.kt @@ -2,6 +2,7 @@ package com.afternote.feature.setting.presentation.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.afternote.core.common.result.runCatchingCancellable import com.afternote.core.domain.repository.MyProfileRepository import com.afternote.core.domain.repository.auth.AuthRepository import com.afternote.feature.setting.domain.SettingAccountRepository @@ -42,7 +43,7 @@ sealed interface WithdrawUiState { * 로컬 토큰은 결과 무관하게 정리하도록 위임돼 있다. 호출처는 토큰 정리 여부를 신경 쓸 필요 없음. */ @HiltViewModel -class SettingViewModel +internal class SettingViewModel @Inject constructor( private val authRepository: AuthRepository, @@ -63,7 +64,7 @@ class SettingViewModel private fun loadProfile() { viewModelScope.launch { - runCatching { myProfileRepository.getMyProfile() } + runCatchingCancellable { myProfileRepository.getMyProfile() } .onSuccess { profile -> _uiState.value = SettingUiState.Success( diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawConfirmScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawConfirmScreen.kt index 73560e2fb..ea165e883 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawConfirmScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/home/WithdrawConfirmScreen.kt @@ -10,7 +10,7 @@ import com.afternote.core.ui.popup.PopupType import com.afternote.feature.setting.presentation.R @Composable -fun WithdrawConfirmScreen( +internal fun WithdrawConfirmScreen( uiState: SettingUiState, onBackClick: () -> Unit, onWithdrawSuccess: () -> Unit, diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/NotificationSettingScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/NotificationSettingScreen.kt index b5a271b26..011c18032 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/NotificationSettingScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/NotificationSettingScreen.kt @@ -47,7 +47,7 @@ import com.afternote.feature.setting.presentation.notification.component.DeviceA import com.afternote.feature.setting.presentation.shared.component.SettingMenuItem @Composable -fun NotificationSettingScreen( +internal fun NotificationSettingScreen( onBack: () -> Unit, onPushNotificationClick: () -> Unit, viewModel: PushNotificationViewModel = hiltViewModel(), diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreen.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreen.kt index 2dcb24789..ea1317618 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreen.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationScreen.kt @@ -13,7 +13,7 @@ import com.afternote.feature.setting.presentation.notification.component.DeviceA import com.afternote.feature.setting.presentation.notification.component.PushToggleSection @Composable -fun PushNotificationScreen( +internal fun PushNotificationScreen( onBack: () -> Unit, viewModel: PushNotificationViewModel = hiltViewModel(), ) { diff --git a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationViewModel.kt b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationViewModel.kt index 4e928b830..f55695787 100644 --- a/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationViewModel.kt +++ b/feature/setting/presentation/src/main/kotlin/com/afternote/feature/setting/presentation/notification/PushNotificationViewModel.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @HiltViewModel -class PushNotificationViewModel +internal class PushNotificationViewModel @Inject constructor( @ApplicationContext private val context: Context, @@ -84,7 +84,7 @@ class PushNotificationViewModel private fun loadMarketingConsents() { viewModelScope.launch { Log.d(TAG, "loadMarketingConsents: start") - runCatching { notificationRepository.getMyMarketingConsents() } + runCatchingCancellable { notificationRepository.getMyMarketingConsents() } .onSuccess { consent -> Log.d(TAG, "loadMarketingConsents: success=$consent") _uiState.update { diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt new file mode 100644 index 000000000..52c92558a --- /dev/null +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt @@ -0,0 +1,368 @@ +package com.afternote.feature.setting.presentation + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelStore +import androidx.test.core.app.ApplicationProvider +import com.afternote.core.common.reporting.ErrorReporter +import com.afternote.core.domain.testing.FakeAuthRepository +import com.afternote.core.domain.testing.FakeMyProfileRepository +import com.afternote.core.model.user.User +import com.afternote.core.model.user.UserMarketingConsent +import com.afternote.core.model.user.UserPushSetting +import com.afternote.feature.setting.domain.testing.FakeSettingAccountRepository +import com.afternote.feature.setting.domain.testing.FakeSettingNotificationRepository +import com.afternote.feature.setting.presentation.account.ConnectedAccountsViewModel +import com.afternote.feature.setting.presentation.home.SettingUiState +import com.afternote.feature.setting.presentation.home.SettingViewModel +import com.afternote.feature.setting.presentation.notification.PushNotificationEvent +import com.afternote.feature.setting.presentation.notification.PushNotificationViewModel +import com.afternote.feature.setting.presentation.profile.ProfileEditEvent +import com.afternote.feature.setting.presentation.profile.ProfileEditUiState +import com.afternote.feature.setting.presentation.profile.ProfileEditViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowLog + +/** + * 설정 화면 ViewModel 의 suspend 저장소 호출이 코루틴 취소를 실패로 둔갑시키지 않는지 검증한다 (#1501). + * + * 취소는 화면 이탈로 [ViewModel.onCleared] 가 불릴 때 실제로 일어나므로, 여기서는 진짜 + * [ViewModelStore] 에 담았다가 [ViewModelStore.clear] 로 끊는다. 저장소 fake 는 응답하지 않고 + * 매달려 있어, 취소가 대기 중인 호출까지 닿았는지를 [PendingRepositoryCall.isCancelled] 로 본다. + * 예외를 직접 던지는 검사에 더해, 실제 ViewModel 종료가 진행 중 요청을 취소하는 경로까지 검증한다. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class SettingCoroutineCancellationTest { + private val dispatcher = StandardTestDispatcher() + + @Before + fun setUp() { + Dispatchers.setMain(dispatcher) + ShadowLog.clear() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `연결 계정 조회 중 화면을 떠나면 오류 문구로 덮지 않는다`() = + runTest(dispatcher) { + val pending = PendingRepositoryCall() + val repository = + FakeSettingAccountRepository().apply { + onGetConnectedAccounts = { pending.await() } + } + val viewModel = ConnectedAccountsViewModel(repository) + val store = storeHolding(viewModel) + runCurrent() + assertTrue(pending.isStarted) + val pendingState = viewModel.uiState.value + + store.clear() + runCurrent() + + assertTrue(pending.isCancelled) + assertEquals(pendingState, viewModel.uiState.value) + assertTrue(viewModel.uiState.value.isLoading) + assertEquals(null, viewModel.uiState.value.errorMessage) + } + + @Test + fun `계정 연결 중 화면을 떠나면 연결 실패 문구를 띄우지 않는다`() = + runTest(dispatcher) { + val pending = PendingRepositoryCall() + val repository = + FakeSettingAccountRepository().apply { + onLinkConnectedAccount = { _, _ -> pending.await() } + } + val viewModel = ConnectedAccountsViewModel(repository) + val store = storeHolding(viewModel) + runCurrent() + + viewModel.link(provider = "google", accessToken = "google-token") + runCurrent() + assertTrue(pending.isStarted) + val pendingState = viewModel.uiState.value + + store.clear() + runCurrent() + + assertTrue(pending.isCancelled) + assertEquals(pendingState, viewModel.uiState.value) + assertEquals(null, viewModel.uiState.value.errorMessage) + } + + @Test + fun `계정 연결 해제 중 화면을 떠나면 해제 실패 문구를 띄우지 않는다`() = + runTest(dispatcher) { + val pending = PendingRepositoryCall() + val repository = + FakeSettingAccountRepository().apply { + onUnlinkConnectedAccount = { pending.await() } + } + val viewModel = ConnectedAccountsViewModel(repository) + val store = storeHolding(viewModel) + runCurrent() + + viewModel.onToggle(provider = "naver", enabled = false) + runCurrent() + assertTrue(pending.isStarted) + val pendingState = viewModel.uiState.value + + store.clear() + runCurrent() + + assertTrue(pending.isCancelled) + assertEquals(pendingState, viewModel.uiState.value) + assertEquals(null, viewModel.uiState.value.errorMessage) + } + + @Test + fun `프로필 편집 조회 중 화면을 떠나면 Error 상태로 바꾸지 않는다`() = + runTest(dispatcher) { + val pending = PendingRepositoryCall() + val repository = + FakeMyProfileRepository().apply { + onGetMyProfile = { pending.await() } + } + val viewModel = ProfileEditViewModel(repository) + val store = storeHolding(viewModel) + runCurrent() + assertTrue(pending.isStarted) + + store.clear() + runCurrent() + + assertTrue(pending.isCancelled) + assertEquals(ProfileEditUiState.Loading, viewModel.uiState.value) + } + + @Test + fun `프로필 수정 중 화면을 떠나면 갱신 표시를 풀거나 실패 이벤트를 보내지 않는다`() = + runTest(dispatcher) { + val pending = PendingRepositoryCall() + val repository = + FakeMyProfileRepository(profile = PROFILE).apply { + onUpdateMyProfile = { _, _, _ -> pending.await() } + } + val viewModel = ProfileEditViewModel(repository) + val store = storeHolding(viewModel) + val events = mutableListOf() + backgroundScope.launch(dispatcher) { viewModel.events.collect { events += it } } + runCurrent() + + viewModel.updateProfile(name = "새 이름", phone = "01011112222") + runCurrent() + assertTrue(pending.isStarted) + + store.clear() + runCurrent() + + assertTrue(pending.isCancelled) + assertEquals( + ProfileEditUiState.Success( + name = PROFILE.name, + phone = PROFILE.phone.orEmpty(), + email = PROFILE.email, + isUpdating = true, + ), + viewModel.uiState.value, + ) + assertTrue(events.isEmpty()) + } + + @Test + fun `설정 홈 프로필 조회 중 화면을 떠나면 Error 상태로 바꾸지 않는다`() = + runTest(dispatcher) { + val pending = PendingRepositoryCall() + val viewModel = + SettingViewModel( + authRepository = FakeAuthRepository.strict(), + myProfileRepository = + FakeMyProfileRepository.strict().apply { + onGetMyProfile = { pending.await() } + }, + accountRepository = FakeSettingAccountRepository.strict(), + ) + val store = storeHolding(viewModel) + runCurrent() + assertTrue(pending.isStarted) + + store.clear() + runCurrent() + + assertTrue(pending.isCancelled) + assertEquals(SettingUiState.Loading, viewModel.uiState.value) + } + + @Test + fun `마케팅 동의 조회 중 화면을 떠나면 상태 진단 어느 것도 남기지 않는다`() = + runTest(dispatcher) { + val pending = PendingRepositoryCall() + val repository = + FakeSettingNotificationRepository().apply { + onGetMyPushSettings = { UserPushSetting(timeLetter = true, mindRecord = false, afterNote = true) } + onGetMyMarketingConsents = { pending.await() } + } + val reporter = RecordingErrorReporter() + val viewModel = pushViewModel(repository, reporter) + val store = storeHolding(viewModel) + val events = mutableListOf() + backgroundScope.launch(dispatcher) { viewModel.events.collect { events += it } } + runCurrent() + + // 푸시 설정 조회는 먼저 정상 종료해 안정된 상태를 만든다. 매달린 것은 마케팅 조회뿐이다. + assertFalse(viewModel.uiState.value.isLoading) + assertTrue(pending.isStarted) + val stableState = viewModel.uiState.value + + store.clear() + runCurrent() + + assertTrue(pending.isCancelled) + assertEquals(stableState, viewModel.uiState.value) + assertTrue(events.isEmpty()) + assertEquals(0, reporter.recordedStages.size) + assertEquals(emptyList(), pushFailureLogs()) + } + + @Test + fun `마케팅 동의 조회는 성공하면 상태에 반영하고 평범한 실패는 로그만 남긴다`() = + runTest(dispatcher) { + val successReporter = RecordingErrorReporter() + val successViewModel = + pushViewModel( + FakeSettingNotificationRepository().apply { + onGetMyPushSettings = { UserPushSetting(timeLetter = false, mindRecord = false, afterNote = false) } + onGetMyMarketingConsents = { UserMarketingConsent(sms = false, email = true, push = true) } + }, + successReporter, + ) + runCurrent() + + assertFalse(successViewModel.uiState.value.isSmsChecked) + assertTrue(successViewModel.uiState.value.isEmailChecked) + assertTrue(successViewModel.uiState.value.isPushChecked) + assertEquals(emptyList(), pushFailureLogs()) + + ShadowLog.clear() + val failureReporter = RecordingErrorReporter() + val failureViewModel = + pushViewModel( + FakeSettingNotificationRepository().apply { + onGetMyPushSettings = { UserPushSetting(timeLetter = false, mindRecord = false, afterNote = false) } + onGetMyMarketingConsents = { error("marketing consents unavailable") } + }, + failureReporter, + ) + val events = mutableListOf() + backgroundScope.launch(dispatcher) { failureViewModel.events.collect { events += it } } + runCurrent() + + // 평범한 실패는 진단 로그 한 줄로 끝난다. 화면 값은 기본값 그대로이고 안내도 띄우지 않는다. + assertEquals(listOf("loadMarketingConsents: failed"), pushFailureLogs()) + assertTrue(failureViewModel.uiState.value.isSmsChecked) + assertTrue(failureViewModel.uiState.value.isEmailChecked) + assertFalse(failureViewModel.uiState.value.isPushChecked) + assertTrue(events.isEmpty()) + assertEquals(0, failureReporter.recordedStages.size) + } + + @Test + fun `Exception 이 아닌 Throwable 실패도 평소대로 오류 상태로 매핑한다`() = + runTest(dispatcher) { + val viewModel = + SettingViewModel( + authRepository = FakeAuthRepository.strict(), + myProfileRepository = + FakeMyProfileRepository.strict().apply { + onGetMyProfile = { throw NonExceptionFailure() } + }, + accountRepository = FakeSettingAccountRepository.strict(), + ) + runCurrent() + + assertEquals(SettingUiState.Error("프로필을 불러올 수 없습니다."), viewModel.uiState.value) + } + + private fun pushViewModel( + repository: FakeSettingNotificationRepository, + reporter: ErrorReporter, + ) = PushNotificationViewModel( + context = ApplicationProvider.getApplicationContext(), + notificationRepository = repository, + errorReporter = reporter, + ) + + /** 푸시 ViewModel 이 남긴 실패 진단 로그의 메시지 목록. 성공 경로의 `Log.d` 는 세지 않는다. */ + private fun pushFailureLogs(): List = + ShadowLog + .getLogsForTag(PUSH_TAG) + .filter { it.type >= Log.ERROR } + .map { it.msg } + + private fun storeHolding(viewModel: ViewModel): ViewModelStore = ViewModelStore().apply { put(STORE_KEY, viewModel) } + + private class RecordingErrorReporter : ErrorReporter { + val recordedStages = mutableListOf() + + override fun writeFailure( + throwable: Throwable, + attributes: Map, + ) { + recordedStages += attributes["stage"] + } + } + + /** `Exception` 을 상속하지 않는 실패. `runCatching` 계열이 `Throwable` 까지 잡는다는 계약을 고정한다. */ + private class NonExceptionFailure : Throwable("profile backend down") + + private companion object { + const val STORE_KEY = "setting-cancellation" + const val PUSH_TAG = "PushNotificationVM" + val PROFILE = User(name = "기존 이름", email = "user@afternote.local", phone = "01000000000", profileImageUrl = null) + } +} + +/** + * 응답하지 않는 저장소 호출. 취소가 이 자리까지 닿았는지를 [isCancelled] 로 드러낸다. + * + * 취소 콜백(`invokeOnCancellation`)은 코루틴이 취소될 때만 호출되므로, 대기 중인 suspend 호출이 + * 실제로 끊겼다는 증거가 된다. + */ +private class PendingRepositoryCall { + @Volatile + var isStarted = false + private set + + @Volatile + var isCancelled = false + private set + + suspend fun await(): T = + suspendCancellableCoroutine { continuation -> + isStarted = true + continuation.invokeOnCancellation { isCancelled = true } + } +} From 4b0a25daeb1f7799be100d214e66fb20f9ba0a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Sun, 27 Sep 2026 13:43:11 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test(setting):=20=EC=B7=A8=EC=86=8C=20?= =?UTF-8?q?=ED=9A=8C=EA=B7=80=EB=A5=BC=20MVI=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EA=B3=84=EC=95=BD=EC=97=90=20=EB=A7=9E=EC=B6=A4=20(#1501)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/SettingCoroutineCancellationTest.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt index 52c92558a..fd9f4e6b3 100644 --- a/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt +++ b/feature/setting/presentation/src/test/kotlin/com/afternote/feature/setting/presentation/SettingCoroutineCancellationTest.kt @@ -17,7 +17,7 @@ import com.afternote.feature.setting.presentation.home.SettingUiState import com.afternote.feature.setting.presentation.home.SettingViewModel import com.afternote.feature.setting.presentation.notification.PushNotificationEvent import com.afternote.feature.setting.presentation.notification.PushNotificationViewModel -import com.afternote.feature.setting.presentation.profile.ProfileEditEvent +import com.afternote.feature.setting.presentation.profile.ProfileEditIntent import com.afternote.feature.setting.presentation.profile.ProfileEditUiState import com.afternote.feature.setting.presentation.profile.ProfileEditViewModel import kotlinx.coroutines.Dispatchers @@ -168,11 +168,9 @@ class SettingCoroutineCancellationTest { } val viewModel = ProfileEditViewModel(repository) val store = storeHolding(viewModel) - val events = mutableListOf() - backgroundScope.launch(dispatcher) { viewModel.events.collect { events += it } } runCurrent() - viewModel.updateProfile(name = "새 이름", phone = "01011112222") + viewModel.onIntent(ProfileEditIntent.UpdateProfile(name = "새 이름", phone = "01011112222")) runCurrent() assertTrue(pending.isStarted) @@ -189,7 +187,6 @@ class SettingCoroutineCancellationTest { ), viewModel.uiState.value, ) - assertTrue(events.isEmpty()) } @Test