Chapter 11 - Testing the ArtViewModel
Now we will code the class ArtViewModelTest, which validates that the ArtViewModel in chapter 10 correctly collects and exposes data from the domain layer (via the GetArtUseCase).
package com.learning.artsnapapp.presentation.viewmodel
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import com.learning.artsnapapp.domain.model.Art
import com.learning.artsnapapp.domain.repository.ArtRepositoryInterface
import com.learning.artsnapapp.domain.usecase.GetArtUseCase
import com.learning.artsnapapp.util.Resource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
/**
* Unit test class for [ArtViewModel].
*
* This class ensures that:
* - The ViewModel correctly collects Flow<List<Art>> from the [GetArtUseCase].
* - The UI state ([ArtUiState]) updates as expected when the use case emits data.
* - No Android framework dependency exists; it runs as a pure Kotlin test.
*
* The test uses a [FakeRepository] that simulates database operations without touching Room.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class ArtViewModelTest {
/**
* Fake repository implementing the domain repository interface.
* Exposes fakeArtList so tests can assert against the same data.
*/
class FakeRepository : ArtRepositoryInterface {
val fakeArtList = listOf(
Art(id = 1, name = "Bensy Dimo", artistName = "Genry Vale", year = "1889", imageUrl = ""),
Art(id = 2, name = "The Spench", artistName = "Kivo", year = "1968", imageUrl = "")
)
override fun getArt(): Flow<List<Art>> = flowOf(fakeArtList)
override fun insertArt(art: Art) {}
override fun deleteArt(art: Art) {}
override fun searchImage(userQuery: String) = flowOf(Resource.success(emptyList()))
}
private lateinit var fakeRepository: FakeRepository
private lateinit var getArtUseCase: GetArtUseCase
private lateinit var viewModel: ArtViewModel
@Before
fun setUp() {
fakeRepository = FakeRepository()
getArtUseCase = GetArtUseCase(fakeRepository)
viewModel = ArtViewModel(getArtUseCase)
}
@Test
fun `arts StateFlow emits expected art list after initialization`() = runTest {
val expected = fakeRepository.fakeArtList
// ArtViewModel.arts is a StateFlow that initially emits emptyList(), then updates.
viewModel.arts.test {
val first = awaitItem() // initial emission from StateFlow (emptyList)
assertThat(first).isEmpty()
val second = awaitItem() // next emission should be the fake data
assertThat(second).isEqualTo(expected)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `errorMessage is null initially`() = runTest {
viewModel.errorMessage.test {
val message = awaitItem()
assertThat(message).isNull()
cancelAndIgnoreRemainingEvents()
}
}
}
First TEST (in the ArtViewModelTest class)
To verify that when the ArtViewModel is initialised, it automatically loads the list of artworks from the repository (through GetArtUseCase) and updates the exposed arts StateFlow correctly.
Flow of control:
ViewModel calls getArtUseCase() inside init → loadArts().
The use case collects the Flow<List<Art>> from the FakeRepository.
The repository emits fakeArtList → ViewModel updates _arts.
The test collects from viewModel.arts and verifies:
First emission → emptyList() (initial StateFlow value)
Second emission → expected fakeArtList
Concepts validated:
Correct propagation of data from Repository → UseCase → ViewModel
ViewModel’s reactive data handling via StateFlow
No side-effects or missing emissions in initialisation logic
Why this is important:
This test confirms that the UI (Compose screen) will automatically show the correct list when the ViewModel starts — crucial for reactive UIs in MVVM.
SECOND TEST (in the ArtViewModel Test class)
To ensure that the ArtViewModel does not emit any spurious error messages upon initialisation.
Flow of control:
ViewModel initialises and triggers loadArts().
Since FakeRepository doesn’t throw exceptions, _errorMessage should stay null.
The test collects viewModel.errorMessage and asserts it’s null.
Concepts validated:
Default UI state correctness after startup
Proper initialisation of reactive error state flow
Why this is important:
Ensures users won’t see any “phantom” errors when the screen first loads — a common issue if state flows are mismanaged or default values are incorrect.
Let us work on the Composable UI and complete the entire flow in the next chapter.