feat(home): 팔로잉 ViewModel을 추가한다

This commit is contained in:
2026-06-25 22:22:32 +09:00
parent 502bbd4f35
commit 2128bbf197
2 changed files with 244 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
package kr.co.vividnext.sodalive.v2.main.home
import android.app.Application
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.test.core.app.ApplicationProvider
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
import io.reactivex.rxjava3.core.Scheduler
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCreatorResponse
import kr.co.vividnext.sodalive.v2.main.home.data.HomeFollowingApi
import kr.co.vividnext.sodalive.v2.main.home.data.HomeFollowingRepository
import kr.co.vividnext.sodalive.v2.main.home.data.HomeFollowingTabResponse
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingUiState
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
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class HomeFollowingViewModelTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val formatter = UtcRelativeTimeTextFormatter { "relative:$it" }
private lateinit var api: FakeHomeFollowingApi
private lateinit var viewModel: HomeFollowingViewModel
@Before
fun setUp() {
setImmediateRxSchedulers()
SharedPreferenceManager.resetForTest()
SharedPreferenceManager.init(context)
SharedPreferenceManager.token = "test-token"
api = FakeHomeFollowingApi()
viewModel = HomeFollowingViewModel(
repository = HomeFollowingRepository(api),
relativeTimeTextFormatter = formatter
)
}
@After
fun tearDown() {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
SharedPreferenceManager.resetForTest()
}
@Test
fun `loadFollowing은 loading 후 content 상태를 발행한다`() {
api.enqueueSuccess(response(followingCreators = listOf(creator())))
viewModel.loadFollowing()
val state = viewModel.followingStateLiveData.requireValue() as HomeFollowingUiState.Content
assertEquals(1L, state.followingCreators.items.single().creatorId)
assertFalse(viewModel.isLoading.requireValue() ?: true)
}
@Test
fun `blank token이면 repository에 null auth header를 전달한다`() {
SharedPreferenceManager.token = " "
api.enqueueSuccess(response(followingCreators = listOf(creator())))
viewModel.loadFollowing()
assertEquals(listOf(null), api.calls)
}
@Test
fun `token이 있으면 repository에 Bearer auth header를 전달한다`() {
SharedPreferenceManager.token = "test-token"
api.enqueueSuccess(response(followingCreators = listOf(creator())))
viewModel.loadFollowing()
assertEquals(listOf("Bearer test-token"), api.calls)
}
@Test
fun `isLoginRequired true 응답은 login required 상태를 발행한다`() {
api.enqueueSuccess(response(isLoginRequired = true, followingCreators = listOf(creator())))
viewModel.loadFollowing()
assertTrue(viewModel.followingStateLiveData.requireValue() is HomeFollowingUiState.LoginRequired)
}
@Test
fun `API success이지만 data가 null이면 error 상태와 toast를 발행한다`() {
api.enqueueSuccess(ApiResponse(true, null, null))
viewModel.loadFollowing()
assertTrue(viewModel.followingStateLiveData.requireValue() is HomeFollowingUiState.Error)
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
}
@Test
fun `API failure throwable이면 error 상태와 toast를 발행한다`() {
api.enqueue(Single.error(IllegalStateException("network")))
viewModel.loadFollowing()
val state = viewModel.followingStateLiveData.requireValue() as HomeFollowingUiState.Error
assertEquals("network", state.message)
assertEquals(R.string.common_error_unknown, viewModel.toastLiveData.requireValue()?.resId)
}
private fun setImmediateRxSchedulers() {
val trampoline = { _: Scheduler -> Schedulers.trampoline() }
RxJavaPlugins.setIoSchedulerHandler(trampoline)
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() }
}
private fun response(
isLoginRequired: Boolean = false,
followingCreators: List<FollowingCreatorResponse> = emptyList()
) = HomeFollowingTabResponse(
isLoginRequired = isLoginRequired,
followingCreators = followingCreators,
onAirLives = emptyList(),
recentChats = emptyList(),
monthlySchedules = emptyList(),
recentNews = emptyList()
)
private fun creator() = FollowingCreatorResponse(
creatorId = 1L,
creatorNickname = "creator",
creatorProfileImageUrl = "https://example.com/creator.png"
)
private fun <T> LiveData<T>.requireValue(): T? {
var value: T? = null
val observer = Observer<T> { value = it }
observeForever(observer)
removeObserver(observer)
return value
}
private class FakeHomeFollowingApi : HomeFollowingApi {
val calls = mutableListOf<String?>()
private val responses = ArrayDeque<Single<ApiResponse<HomeFollowingTabResponse>>>()
fun enqueueSuccess(response: HomeFollowingTabResponse) {
enqueueSuccess(ApiResponse(true, response, null))
}
fun enqueueSuccess(response: ApiResponse<HomeFollowingTabResponse>) {
enqueue(Single.just(response))
}
fun enqueue(response: Single<ApiResponse<HomeFollowingTabResponse>>) {
responses.addLast(response)
}
override fun getFollowing(authHeader: String?): Single<ApiResponse<HomeFollowingTabResponse>> {
calls.add(authHeader)
return responses.removeFirst()
}
}
}