diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeActivity.kt index 24fcdeed0..8536e6a29 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeActivity.kt @@ -12,6 +12,7 @@ import com.festago.festago.presentation.ui.home.festivallist.FestivalListFragmen import com.festago.festago.presentation.ui.home.mypage.MyPageFragment import com.festago.festago.presentation.ui.home.ticketlist.TicketListFragment import com.festago.festago.presentation.ui.signin.SignInActivity +import com.festago.festago.presentation.util.repeatOnStarted import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -48,12 +49,14 @@ class HomeActivity : AppCompatActivity() { } private fun initObserve() { - vm.event.observe(this) { event -> - when (event) { - is HomeEvent.ShowFestivalList -> showFestivalList() - is HomeEvent.ShowTicketList -> showTicketList() - is HomeEvent.ShowMyPage -> showMyPage() - is HomeEvent.ShowSignIn -> showSignIn() + repeatOnStarted(this) { + vm.event.collect { event -> + when (event) { + is HomeEvent.ShowFestivalList -> showFestivalList() + is HomeEvent.ShowTicketList -> showTicketList() + is HomeEvent.ShowMyPage -> showMyPage() + is HomeEvent.ShowSignIn -> showSignIn() + } } } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeViewModel.kt index 27944a088..829d9b924 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/HomeViewModel.kt @@ -1,27 +1,32 @@ package com.festago.festago.presentation.ui.home import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.festago.festago.presentation.ui.home.HomeItemType.FESTIVAL_LIST import com.festago.festago.presentation.ui.home.HomeItemType.MY_PAGE import com.festago.festago.presentation.ui.home.HomeItemType.TICKET_LIST -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData import com.festago.festago.repository.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor(private val authRepository: AuthRepository) : ViewModel() { - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() fun loadHomeItem(homeItemType: HomeItemType) { - when { - homeItemType == FESTIVAL_LIST -> _event.setValue(HomeEvent.ShowFestivalList) - !authRepository.isSigned -> _event.setValue(HomeEvent.ShowSignIn) - homeItemType == TICKET_LIST -> _event.setValue(HomeEvent.ShowTicketList) - homeItemType == MY_PAGE -> _event.setValue(HomeEvent.ShowMyPage) + viewModelScope.launch { + when { + homeItemType == FESTIVAL_LIST -> _event.emit(HomeEvent.ShowFestivalList) + !authRepository.isSigned -> _event.emit(HomeEvent.ShowSignIn) + homeItemType == TICKET_LIST -> _event.emit(HomeEvent.ShowTicketList) + homeItemType == MY_PAGE -> _event.emit(HomeEvent.ShowMyPage) + } } } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListFragment.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListFragment.kt index 7918bc185..c516eabaa 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListFragment.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListFragment.kt @@ -12,6 +12,7 @@ import androidx.fragment.app.viewModels import com.festago.festago.R import com.festago.festago.databinding.FragmentTicketListBinding import com.festago.festago.presentation.ui.ticketentry.TicketEntryActivity +import com.festago.festago.presentation.util.repeatOnStarted import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -44,12 +45,16 @@ class TicketListFragment : Fragment(R.layout.fragment_ticket_list) { } private fun initObserve() { - vm.uiState.observe(viewLifecycleOwner) { - binding.uiState = it - updateUi(it) + repeatOnStarted(viewLifecycleOwner) { + vm.uiState.collect { + binding.uiState = it + updateUi(it) + } } - vm.event.observe(viewLifecycleOwner) { event -> - handleEvent(event) + repeatOnStarted(viewLifecycleOwner) { + vm.event.collect { event -> + handleEvent(event) + } } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModel.kt index e6e8b27ea..ce15bfd2b 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModel.kt @@ -1,15 +1,17 @@ package com.festago.festago.presentation.ui.home.ticketlist -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData import com.festago.festago.repository.TicketRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -19,11 +21,11 @@ class TicketListViewModel @Inject constructor( private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData(TicketListUiState.Loading) - val uiState: LiveData = _uiState + private val _uiState = MutableStateFlow(TicketListUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() fun loadCurrentTickets() { viewModelScope.launch { @@ -40,7 +42,9 @@ class TicketListViewModel @Inject constructor( } fun showTicketEntry(ticketId: Long) { - _event.setValue(TicketListEvent.ShowTicketEntry(ticketId)) + viewModelScope.launch { + _event.emit(TicketListEvent.ShowTicketEntry(ticketId)) + } } companion object { diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryActivity.kt index bc60b476b..46019fce1 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryActivity.kt @@ -6,6 +6,7 @@ import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.festago.festago.databinding.ActivityTicketHistoryBinding +import com.festago.festago.presentation.util.repeatOnStarted import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -30,17 +31,19 @@ class TicketHistoryActivity : AppCompatActivity() { } private fun initObserve() { - vm.uiState.observe(this) { uiState -> - when (uiState) { - is TicketHistoryUiState.Loading, - is TicketHistoryUiState.Error, - -> Unit - - is TicketHistoryUiState.Success -> { - adapter.submitList(uiState.tickets) + repeatOnStarted(this) { + vm.uiState.collect { uiState -> + when (uiState) { + is TicketHistoryUiState.Loading, + is TicketHistoryUiState.Error, + -> Unit + + is TicketHistoryUiState.Success -> { + adapter.submitList(uiState.tickets) + } } + binding.uiState = uiState } - binding.uiState = uiState } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModel.kt index 9a5e7a077..f1cf12e61 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModel.kt @@ -1,13 +1,14 @@ package com.festago.festago.presentation.ui.tickethistory -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure import com.festago.festago.repository.TicketRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -17,14 +18,13 @@ class TicketHistoryViewModel @Inject constructor( private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData() - val uiState: LiveData = _uiState + private val _uiState = MutableStateFlow(TicketHistoryUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() - fun loadTicketHistories(size: Int = 100, refresh: Boolean = false) { + fun loadTicketHistories(size: Int = SIZE_TICKET_HISTORY, refresh: Boolean = false) { if (!refresh && uiState.value is TicketHistoryUiState.Success) return viewModelScope.launch { - _uiState.value = TicketHistoryUiState.Loading ticketRepository.loadHistoryTickets(size) .onSuccess { tickets -> _uiState.value = TicketHistoryUiState.Success( @@ -42,5 +42,6 @@ class TicketHistoryViewModel @Inject constructor( companion object { private const val KEY_LOAD_TICKET_HISTORIES_LOG = "ticket_histories" + private const val SIZE_TICKET_HISTORY = 100 } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt index 790cd9abe..6bb7ebd39 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveActivity.kt @@ -22,6 +22,7 @@ import com.festago.festago.presentation.ui.ticketreserve.adapter.TicketReserveAd import com.festago.festago.presentation.ui.ticketreserve.adapter.TicketReserveHeaderAdapter import com.festago.festago.presentation.ui.ticketreserve.bottomsheet.BottomSheetReservationTicketArg import com.festago.festago.presentation.ui.ticketreserve.bottomsheet.TicketReserveBottomSheetFragment +import com.festago.festago.presentation.util.repeatOnStarted import dagger.hilt.android.AndroidEntryPoint import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -53,12 +54,16 @@ class TicketReserveActivity : AppCompatActivity() { } private fun initObserve() { - vm.uiState.observe(this) { uiState -> - updateUi(uiState) - binding.uiState = uiState + repeatOnStarted(this) { + vm.uiState.collect { uiState -> + updateUi(uiState) + binding.uiState = uiState + } } - vm.event.observe(this) { event -> - handleEvent(event) + repeatOnStarted(this) { + vm.event.collect { event -> + handleEvent(event) + } } } @@ -89,7 +94,7 @@ class TicketReserveActivity : AppCompatActivity() { id = it.id, remainAmount = it.remainAmount, ticketType = it.ticketType, - totalAmount = it.totalAmount + totalAmount = it.totalAmount, ) }, ).show(supportFragmentManager, TicketReserveBottomSheetFragment::class.java.name) diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModel.kt index e295c6dc7..46f327f9f 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModel.kt @@ -1,19 +1,21 @@ package com.festago.festago.presentation.ui.ticketreserve -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure import com.festago.festago.model.ReservationStage -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData import com.festago.festago.repository.AuthRepository import com.festago.festago.repository.FestivalRepository import com.festago.festago.repository.ReservationTicketRepository import com.festago.festago.repository.TicketRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.time.LocalDateTime import javax.inject.Inject @@ -26,28 +28,26 @@ class TicketReserveViewModel @Inject constructor( private val authRepository: AuthRepository, private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData(TicketReserveUiState.Loading) - val uiState: LiveData = _uiState + private val _uiState = MutableStateFlow(TicketReserveUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event + private val _event = MutableSharedFlow() + val event: SharedFlow = _event.asSharedFlow() fun loadReservation(festivalId: Long = 0, refresh: Boolean = false) { if (!refresh && uiState.value is TicketReserveUiState.Success) return viewModelScope.launch { festivalRepository.loadFestivalDetail(festivalId) .onSuccess { - _uiState.setValue( - TicketReserveUiState.Success( - festival = ReservationFestivalUiState( - id = it.id, - name = it.name, - thumbnail = it.thumbnail, - endDate = it.endDate, - startDate = it.startDate, - ), - stages = it.reservationStages.toTicketReserveItems(), + _uiState.value = TicketReserveUiState.Success( + festival = ReservationFestivalUiState( + id = it.id, + name = it.name, + thumbnail = it.thumbnail, + endDate = it.endDate, + startDate = it.startDate, ), + stages = it.reservationStages.toTicketReserveItems(), ) }.onFailure { _uiState.value = TicketReserveUiState.Error @@ -64,17 +64,17 @@ class TicketReserveViewModel @Inject constructor( if (authRepository.isSigned) { reservationTicketRepository.loadTicketTypes(stageId) .onSuccess { tickets -> - _event.setValue( + _event.emit( TicketReserveEvent.ShowTicketTypes( stageStartTime, tickets, ), ) }.onFailure { - _uiState.setValue(TicketReserveUiState.Error) + _uiState.value = TicketReserveUiState.Error } } else { - _event.setValue(TicketReserveEvent.ShowSignIn) + _event.emit(TicketReserveEvent.ShowSignIn) } } } @@ -83,9 +83,9 @@ class TicketReserveViewModel @Inject constructor( viewModelScope.launch { ticketRepository.reserveTicket(ticketId) .onSuccess { - _event.setValue(TicketReserveEvent.ReserveTicketSuccess(it)) + _event.emit(TicketReserveEvent.ReserveTicketSuccess(it)) }.onFailure { - _event.setValue(TicketReserveEvent.ReserveTicketFailed) + _event.emit(TicketReserveEvent.ReserveTicketFailed) } } } diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/HomeViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/HomeViewModelTest.kt index 5e7b174f7..f6f245d06 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/HomeViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/HomeViewModelTest.kt @@ -1,6 +1,6 @@ package com.festago.festago.presentation.ui.home -import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import app.cash.turbine.test import com.festago.festago.repository.AuthRepository import io.mockk.every import io.mockk.mockk @@ -8,20 +8,17 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test class HomeViewModelTest { private lateinit var vm: HomeViewModel private lateinit var authRepository: AuthRepository - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { @@ -37,74 +34,85 @@ class HomeViewModelTest { } @Test - fun `축제 목록을 요청했을 때 토큰이 있으면 축제 목록이 보인다`() { + fun `축제 목록을 요청했을 때 토큰이 있으면 축제 목록이 보인다`() = runTest { // given every { authRepository.isSigned } returns true // when - vm.loadHomeItem(HomeItemType.FESTIVAL_LIST) + vm.event.test { + vm.loadHomeItem(HomeItemType.FESTIVAL_LIST) - // then - assertThat(vm.event.getValue() is HomeEvent.ShowFestivalList).isTrue + // then + assertThat(awaitItem()).isExactlyInstanceOf(HomeEvent.ShowFestivalList::class.java) + } } @Test - fun `축제 목록을 요청했을 때 토큰이 없어도 축제 목록이 보인다`() { + fun `축제 목록을 요청했을 때 토큰이 없어도 축제 목록이 보인다`() = runTest { // given every { authRepository.isSigned } returns false - // when - vm.loadHomeItem(HomeItemType.FESTIVAL_LIST) + vm.event.test { + // when + vm.loadHomeItem(HomeItemType.FESTIVAL_LIST) - // then - assertThat(vm.event.getValue() is HomeEvent.ShowFestivalList).isTrue + // then + assertThat(awaitItem()).isExactlyInstanceOf(HomeEvent.ShowFestivalList::class.java) + } } @Test - fun `티켓 목록을 요청했을 때 토큰이 있으면 티켓 목록 보기 이벤트가 발생한다`() { + fun `티켓 목록을 요청했을 때 토큰이 있으면 티켓 목록 보기 이벤트가 발생한다`() = runTest { // given every { authRepository.isSigned } returns true - // when - vm.loadHomeItem(HomeItemType.TICKET_LIST) + vm.event.test { + // when + vm.loadHomeItem(HomeItemType.TICKET_LIST) - // then - assertThat(vm.event.getValue() is HomeEvent.ShowTicketList).isTrue + // then + assertThat(awaitItem()).isExactlyInstanceOf(HomeEvent.ShowTicketList::class.java) + } } @Test - fun `티켓 목록을 요청했을 때 토큰이 있으면 로그인 보기 이벤트가 발생한다`() { + fun `티켓 목록을 요청했을 때 토큰이 있으면 로그인 보기 이벤트가 발생한다`() = runTest { // given every { authRepository.isSigned } returns false - // when - vm.loadHomeItem(HomeItemType.TICKET_LIST) + vm.event.test { + // when + vm.loadHomeItem(HomeItemType.TICKET_LIST) - // then - assertThat(vm.event.getValue() is HomeEvent.ShowSignIn).isTrue + // then + assertThat(awaitItem()).isExactlyInstanceOf(HomeEvent.ShowSignIn::class.java) + } } @Test - fun `마이페이지를 요청했을 때 토큰이 있으면 마이페이지 보기 이벤트가 발생한다`() { + fun `마이페이지를 요청했을 때 토큰이 있으면 마이페이지 보기 이벤트가 발생한다`() = runTest { // given every { authRepository.isSigned } returns true - // when - vm.loadHomeItem(HomeItemType.MY_PAGE) + vm.event.test { + // when + vm.loadHomeItem(HomeItemType.MY_PAGE) - // then - assertThat(vm.event.getValue() is HomeEvent.ShowMyPage).isTrue + // then + assertThat(awaitItem()).isExactlyInstanceOf(HomeEvent.ShowMyPage::class.java) + } } @Test - fun `마이페이즈를 요청했을 때 토큰이 없으면 로그인 보기 이벤트가 발생한다`() { + fun `마이페이즈를 요청했을 때 토큰이 없으면 로그인 보기 이벤트가 발생한다`() = runTest { // given every { authRepository.isSigned } returns false - // when - vm.loadHomeItem(HomeItemType.MY_PAGE) + vm.event.test { + // when + vm.loadHomeItem(HomeItemType.MY_PAGE) - // then - assertThat(vm.event.getValue() is HomeEvent.ShowSignIn).isTrue + assertThat(awaitItem()).isExactlyInstanceOf(HomeEvent.ShowSignIn::class.java) + } } } diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModelTest.kt index ff7fa0064..af3fe8dda 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/home/ticketlist/TicketListViewModelTest.kt @@ -1,6 +1,6 @@ package com.festago.festago.presentation.ui.home.ticketlist -import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import app.cash.turbine.test import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.model.Ticket import com.festago.festago.presentation.fixture.TicketFixture @@ -12,12 +12,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain -import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.SoftAssertions import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test class TicketListViewModelTest { @@ -25,9 +24,6 @@ class TicketListViewModelTest { private lateinit var ticketRepository: TicketRepository private lateinit var analyticsHelper: AnalyticsHelper - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { @@ -58,10 +54,10 @@ class TicketListViewModelTest { assertThat(vm.uiState.value).isInstanceOf(TicketListUiState.Success::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isEqualTo(true) - assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(false) // and val actual = (vm.uiState.value as TicketListUiState.Success).tickets @@ -85,10 +81,10 @@ class TicketListViewModelTest { assertThat(vm.uiState.value).isInstanceOf(TicketListUiState.Success::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isEqualTo(true) - assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(false) // and val actual = (vm.uiState.value as TicketListUiState.Success).tickets @@ -114,10 +110,10 @@ class TicketListViewModelTest { assertThat(vm.uiState.value).isInstanceOf(TicketListUiState.Error::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(true) } softly.assertAll() } @@ -140,27 +136,32 @@ class TicketListViewModelTest { assertThat(vm.uiState.value).isInstanceOf(TicketListUiState.Loading::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(true) - assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(false) } softly.assertAll() } @Test - fun `티켓 제시를 요청하면 이벤트가 발생한다`() { + fun `1번 티켓 제시를 요청하면 1번 티켓 제시 화면 보여주기 이벤트가 발생한다`() = runTest { // given - // when - vm.showTicketEntry(1L) + vm.event.test { + // when + vm.showTicketEntry(1L) - // then - assertThat(vm.event.getValue()).isInstanceOf(TicketListEvent.ShowTicketEntry::class.java) + // then + val softly = SoftAssertions().apply { + val event = awaitItem() + assertThat(event).isExactlyInstanceOf(TicketListEvent.ShowTicketEntry::class.java) - // and - val actual = (vm.event.getValue() as TicketListEvent.ShowTicketEntry).ticketId - val expected = 1L - assertThat(actual).isEqualTo(expected) + // and + val ticketId = (event as? TicketListEvent.ShowTicketEntry)?.ticketId + assertThat(ticketId).isEqualTo(1L) + } + softly.assertAll() + } } } diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt index fc884afee..d6708dc56 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/selectschool/SelectSchoolViewModelTest.kt @@ -33,7 +33,7 @@ class SelectSchoolViewModelTest { analyticsHelper = mockk(relaxed = true) vm = SelectSchoolViewModel( schoolRepository = schoolRepository, - analyticsHelper = analyticsHelper + analyticsHelper = analyticsHelper, ) } @@ -128,6 +128,6 @@ class SelectSchoolViewModelTest { private val fakeSchools = listOf( School(id = fakeSchoolId, domain = "scripta", name = "Charley Sullivan"), - School(id = 8930, domain = "movet", name = "Juliette Fleming") + School(id = 8930, domain = "movet", name = "Juliette Fleming"), ) } diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt index 5b50e6381..672066619 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/studentverification/StudentVerificationViewModelTest.kt @@ -207,7 +207,6 @@ class StudentVerificationViewModelTest { // then assertThat(awaitItem()).isEqualTo(StudentVerificationEvent.VerificationSuccess) - cancelAndIgnoreRemainingEvents() } } @@ -231,7 +230,6 @@ class StudentVerificationViewModelTest { // then assertThat(awaitItem()).isExactlyInstanceOf(StudentVerificationEvent.VerificationFailure::class.java) - cancelAndIgnoreRemainingEvents() } } @@ -252,7 +250,6 @@ class StudentVerificationViewModelTest { // then assertThat(awaitItem()).isEqualTo(StudentVerificationEvent.VerificationTimeOut) - cancelAndIgnoreRemainingEvents() } } diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModelTest.kt index 700f2125e..9e90b6955 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/tickethistory/TicketHistoryViewModelTest.kt @@ -1,6 +1,5 @@ package com.festago.festago.presentation.ui.tickethistory -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.model.Ticket import com.festago.festago.presentation.fixture.TicketFixture @@ -16,7 +15,6 @@ import kotlinx.coroutines.test.setMain import org.assertj.core.api.SoftAssertions import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test class TicketHistoryViewModelTest { @@ -25,9 +23,6 @@ class TicketHistoryViewModelTest { private lateinit var ticketRepository: TicketRepository private lateinit var analyticsHelper: AnalyticsHelper - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { @@ -129,10 +124,10 @@ class TicketHistoryViewModelTest { assertThat(vm.uiState.value).isInstanceOf(TicketHistoryUiState.Loading::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isFalse - assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isFalse - assertThat(vm.uiState.value?.shouldShowLoading).isTrue - assertThat(vm.uiState.value?.shouldShowError).isFalse + assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isFalse + assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isFalse + assertThat(vm.uiState.value.shouldShowLoading).isTrue + assertThat(vm.uiState.value.shouldShowError).isFalse } softly.assertAll() @@ -155,12 +150,11 @@ class TicketHistoryViewModelTest { assertThat(vm.uiState.value).isInstanceOf(TicketHistoryUiState.Error::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccessWithTickets).isFalse - assertThat(vm.uiState.value?.shouldShowSuccessAndEmpty).isFalse - assertThat(vm.uiState.value?.shouldShowLoading).isFalse - assertThat(vm.uiState.value?.shouldShowError).isTrue + assertThat(vm.uiState.value.shouldShowSuccessWithTickets).isFalse + assertThat(vm.uiState.value.shouldShowSuccessAndEmpty).isFalse + assertThat(vm.uiState.value.shouldShowLoading).isFalse + assertThat(vm.uiState.value.shouldShowError).isTrue } - softly.assertAll() } diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModelTest.kt index a013a2485..552e532f0 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketreserve/TicketReserveViewModelTest.kt @@ -1,6 +1,6 @@ package com.festago.festago.presentation.ui.ticketreserve -import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import app.cash.turbine.test import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.model.Reservation import com.festago.festago.model.ReservationStage @@ -16,10 +16,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.assertj.core.api.AssertionsForClassTypes.assertThat +import org.assertj.core.api.SoftAssertions +import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import java.time.LocalDate import java.time.LocalDateTime @@ -59,9 +62,6 @@ class TicketReserveViewModelTest { number = 1, ) - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { @@ -80,6 +80,12 @@ class TicketReserveViewModelTest { ) } + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + @Test fun `예약 정보를 불러오면 성공 이벤트가 발생하고 리스트를 반환한다`() { // given @@ -143,7 +149,7 @@ class TicketReserveViewModelTest { } @Test - fun `특정 공연의 티켓 타입을 보여주는 이벤트가 발생하면 해당 공연의 티켓 타입을 보여준다`() { + fun `특정 공연의 티켓 타입을 보여주는 이벤트가 발생하면 해당 공연의 티켓 타입을 보여준다`() = runTest { // given coEvery { reservationTicketRepository.loadTicketTypes(1) @@ -151,21 +157,25 @@ class TicketReserveViewModelTest { Result.success(fakeReservationTickets) } - coEvery { - authRepository.isSigned - } answers { + coEvery { authRepository.isSigned } answers { true } - // when - vm.showTicketTypes(1, LocalDateTime.MIN) + vm.event.test { + // when + vm.showTicketTypes(1, LocalDateTime.MIN) - // then - assertThat(vm.event.getValue()).isInstanceOf(TicketReserveEvent.ShowTicketTypes::class.java) + // then + val softly = SoftAssertions().apply { + val event = awaitItem() + assertThat(event).isExactlyInstanceOf(TicketReserveEvent.ShowTicketTypes::class.java) - // and - val event = vm.event.getValue() as TicketReserveEvent.ShowTicketTypes - assertThat(event.tickets).isEqualTo(fakeReservationTickets) + // and + val actual = (event as? TicketReserveEvent.ShowTicketTypes)?.tickets + assertThat(actual).isEqualTo(fakeReservationTickets) + } + softly.assertAll() + } } @Test @@ -187,22 +197,25 @@ class TicketReserveViewModelTest { } @Test - fun `티켓 유형을 선택하고 예약하면 예약 성공 이벤트가 발생한다`() { + fun `티켓 유형을 선택하고 예약하면 예약 성공 이벤트가 발생한다`() = runTest { // given coEvery { ticketRepository.reserveTicket(any()) } answers { Result.success(fakeReservedTicket) } - // when - vm.reserveTicket(0) - // then - assertThat(vm.event.getValue()).isInstanceOf(TicketReserveEvent.ReserveTicketSuccess::class.java) + vm.event.test { + // when + vm.reserveTicket(0) + + // then + assertThat(awaitItem()).isExactlyInstanceOf(TicketReserveEvent.ReserveTicketSuccess::class.java) + } } @Test - fun `티켓 유형을 선택하고 예약하는 것을 실패하면 예약 실패 이벤트가 발생한다`() { + fun `티켓 유형을 선택하고 예약하는 것을 실패하면 예약 실패 이벤트가 발생한다`() = runTest { // given coEvery { ticketRepository.reserveTicket(0) @@ -210,10 +223,12 @@ class TicketReserveViewModelTest { Result.failure(Exception()) } - // when - vm.reserveTicket(0) + vm.event.test { + // when + vm.reserveTicket(0) - // then - assertThat(vm.event.getValue()).isEqualTo(TicketReserveEvent.ReserveTicketFailed) + // then + assertThat(awaitItem()).isExactlyInstanceOf(TicketReserveEvent.ReserveTicketFailed::class.java) + } } }