Unit tests for Android ViewModels and repositories require tools to handle coroutines, StateFlow, and LiveData properly. This guide covers the essential JUnit 5 + Mockito + coroutine test setup.
Dependencies
// build.gradle.kts (test dependencies)
testImplementation("junit:junit:4.13.2")
// OR JUnit 5:
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
testImplementation("app.cash.turbine:turbine:1.1.0") // Flow testing
testImplementation("androidx.arch.core:core-testing:2.2.0") // InstantTaskExecutorRule
TestCoroutineScheduler and runTest
runTest is the standard way to test suspending functions:
class ArticleRepositoryTest {
@Test
fun `fetch article returns correct data`() = runTest {
val fakeApi = FakeArticleApi(articles = listOf(testArticle))
val repository = ArticleRepository(fakeApi, FakeArticleDao())
val result = repository.getArticle("a1")
assertEquals(testArticle, result)
}
@Test
fun `fetch article with delay completes`() = runTest {
val fakeApi = FakeSlowApi(delayMs = 5000) // simulates 5s network delay
val repository = ArticleRepository(fakeApi, FakeArticleDao())
// runTest auto-advances virtual time — this completes instantly in tests
val result = repository.getArticle("a1")
assertNotNull(result)
}
}
Testing ViewModels with StateFlow
class ArticleViewModelTest {
// Replace Main dispatcher with a test dispatcher
private val testDispatcher = StandardTestDispatcher()
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `loading article updates state`() = runTest {
val repository = FakeArticleRepository().apply {
seedArticles(testArticle)
}
val viewModel = ArticleViewModel(repository)
viewModel.loadArticle("a1")
testDispatcher.scheduler.advanceUntilIdle() // run all pending coroutines
assertEquals(testArticle.title, viewModel.state.value.title)
assertFalse(viewModel.state.value.isLoading)
}
@Test
fun `error loading sets error state`() = runTest {
val repository = FakeArticleRepository().apply {
setError(IOException("Network error"))
}
val viewModel = ArticleViewModel(repository)
viewModel.loadArticle("a1")
testDispatcher.scheduler.advanceUntilIdle()
assertNotNull(viewModel.state.value.error)
}
}
Turbine: Testing Flows
Turbine simplifies testing Flow and StateFlow emissions:
@Test
fun `article list emits loading then content`() = runTest {
val viewModel = ArticleListViewModel(FakeArticleRepository())
viewModel.state.test {
val initial = awaitItem()
assertTrue(initial.isLoading)
viewModel.loadArticles()
val loaded = awaitItem()
assertFalse(loaded.isLoading)
assertTrue(loaded.articles.isNotEmpty())
cancelAndIgnoreRemainingEvents()
}
}
Mockito Kotlin DSL
@Test
fun `bookmark sends correct api call`() = runTest {
val mockApi = mock<ArticleApi>()
whenever(mockApi.bookmark(any())).thenReturn(Unit)
val repository = ArticleRepository(mockApi, FakeArticleDao())
repository.bookmark("a1")
verify(mockApi).bookmark("a1")
verifyNoMoreInteractions(mockApi)
}
@Test
fun `api error propagates as exception`() = runTest {
val mockApi = mock<ArticleApi>()
whenever(mockApi.fetchArticles()).thenThrow(IOException("timeout"))
val repository = ArticleRepository(mockApi, FakeArticleDao())
assertThrows<IOException> {
repository.getArticles()
}
}
CoroutineRule (JUnit 4 Rule)
Abstract the dispatcher setup into a reusable rule:
class CoroutineRule(
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
// Usage in test:
class MyViewModelTest {
@get:Rule val coroutineRule = CoroutineRule()
@Test
fun `some test`() = coroutineRule.testDispatcher.runBlockingTest {
// test coroutine code
}
}
Key Takeaways
| Tool | Purpose |
|---|---|
runTest | Execute suspend functions in tests with virtual time |
StandardTestDispatcher | Control coroutine execution manually; use with advanceUntilIdle() |
Dispatchers.setMain | Replace Main dispatcher in ViewModel tests |
Turbine .test {} | Assert Flow emissions in order |
mockito-kotlin | Idiomatic Kotlin API for Mockito |
whenever(...).thenReturn(...) | Stub return values on mocks |
verify(mock).method(args) | Assert specific calls were made on mocks |