Skip to content

Commit

Permalink
[#465] Test "SharedFlow" execution in Composable with Robolectric wit…
Browse files Browse the repository at this point in the history
…hout switching to StateFlow
  • Loading branch information
luongvo committed May 19, 2023
1 parent b9304c7 commit 60e4e49
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ abstract class BaseViewModel : ViewModel() {
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<IsLoading> = _isLoading

protected val _error = MutableStateFlow<Throwable?>(null)
val error: StateFlow<Throwable?> = _error
protected val _error = MutableSharedFlow<Throwable>()
val error: SharedFlow<Throwable> = _error

protected val _navigator = MutableSharedFlow<AppDestination>()
val navigator: SharedFlow<AppDestination> = _navigator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,14 @@ fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
navigator: (destination: AppDestination) -> Unit,
) {
val context = LocalContext.current
viewModel.error.collectAsEffect { e -> e.showToast(context) }
viewModel.navigator.collectAsEffect { destination -> navigator(destination) }

val isLoading: IsLoading by viewModel.isLoading.collectAsStateWithLifecycle()
val uiModels: List<UiModel> by viewModel.uiModels.collectAsStateWithLifecycle()
val isFirstTimeLaunch: Boolean by viewModel.isFirstTimeLaunch.collectAsStateWithLifecycle()

val context = LocalContext.current
val error: Throwable? by viewModel.error.collectAsStateWithLifecycle()
error?.showToast(context)
LaunchedEffect(isFirstTimeLaunch) {
if (isFirstTimeLaunch) {
context.showToast("This is the first time launch")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.junit.runner.Description

@OptIn(ExperimentalCoroutinesApi::class)
class CoroutineTestRule(
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
var testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {

val testDispatcherProvider = object : DispatchersProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ class HomeScreenTest {
)

private val mockGetModelsUseCase: GetModelsUseCase = mockk()
private val mockIsFirstTimeLaunchPreferencesUseCase: IsFirstTimeLaunchPreferencesUseCase = mockk()
private val mockUpdateFirstTimeLaunchPreferencesUseCase: UpdateFirstTimeLaunchPreferencesUseCase = mockk()
private val mockIsFirstTimeLaunchPreferencesUseCase: IsFirstTimeLaunchPreferencesUseCase =
mockk()
private val mockUpdateFirstTimeLaunchPreferencesUseCase: UpdateFirstTimeLaunchPreferencesUseCase =
mockk()

private lateinit var viewModel: HomeViewModel
private var expectedAppDestination: AppDestination? = null
Expand All @@ -70,12 +72,15 @@ class HomeScreenTest {

@Test
fun `When entering the Home screen and loading the list item failure, it shows the corresponding error`() {
coroutinesRule.testDispatcher = StandardTestDispatcher()

val error = Exception()
every { mockGetModelsUseCase() } returns flow { throw error }
initViewModel()

initComposable {
onNodeWithText("Home").assertIsDisplayed()
composeRule.waitForIdle()
coroutinesRule.testDispatcher.scheduler.advanceUntilIdle()

ShadowToast.showedToast(activity.getString(R.string.error_generic)) shouldBe true
}
Expand All @@ -89,13 +94,13 @@ class HomeScreenTest {
}

private fun initComposable(
testBody: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>.() -> Unit
testBody: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>.() -> Unit,
) {
composeRule.activity.setContent {
ComposeTheme {
HomeScreen(
viewModel = viewModel,
navigator = { destination -> expectedAppDestination = destination }
navigator = { destination -> expectedAppDestination = destination },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.junit.runner.Description

@OptIn(ExperimentalCoroutinesApi::class)
class CoroutineTestRule(
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
var testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {

val testDispatcherProvider = object : DispatchersProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,84 @@ import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.*
import androidx.test.ext.junit.rules.ActivityScenarioRule
import co.nimblehq.template.compose.R
import co.nimblehq.template.compose.domain.model.Model
import co.nimblehq.template.compose.domain.usecase.UseCase
import co.nimblehq.template.compose.test.CoroutineTestRule
import co.nimblehq.template.compose.ui.AppDestination
import co.nimblehq.template.compose.ui.screens.MainActivity
import co.nimblehq.template.compose.ui.theme.ComposeTheme
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.*
import org.junit.*
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.shadows.ShadowToast

@RunWith(RobolectricTestRunner::class)
class HomeScreenTest {

private val coroutinesRule = CoroutineTestRule()

@get:Rule
val composeRule = createAndroidComposeRule<MainActivity>()

private val mockUseCase: UseCase = mockk()

private lateinit var viewModel: HomeViewModel
private var expectedAppDestination: AppDestination? = null

@Before
fun setUp() {
// TODO more setup logic here
every { mockUseCase() } returns flowOf(
listOf(Model(1), Model(2), Model(3))
)

initViewModel()
}

@Test
fun `When entering the Home screen, it shows UI correctly`() = initComposable {
onNodeWithText(activity.getString(R.string.app_name)).assertIsDisplayed()
}

@Test
fun `When entering the Home screen and loading the data failure, it shows the corresponding error`() {
coroutinesRule.testDispatcher = StandardTestDispatcher()

val error = Exception()
every { mockUseCase() } returns flow { throw error }
initViewModel()

initComposable {
composeRule.waitForIdle()
coroutinesRule.testDispatcher.scheduler.advanceUntilIdle()

ShadowToast.showedToast(activity.getString(R.string.error_generic)) shouldBe true
}
}

private fun initComposable(
testBody: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>.() -> Unit
testBody: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>.() -> Unit,
) {
composeRule.activity.setContent {
ComposeTheme {
HomeScreen(
viewModel = viewModel,
navigator = { destination -> expectedAppDestination = destination }
)
}
}
testBody(composeRule)
}

private fun initViewModel() {
viewModel = HomeViewModel(
coroutinesRule.testDispatcherProvider,
mockUseCase,
)
}
}

0 comments on commit 60e4e49

Please sign in to comment.