androidengineers.Book a session

Testing Architecture

JUnit/Mockito/KotlinX Coroutines Test

article25 minMedium

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

ToolPurpose
runTestExecute suspend functions in tests with virtual time
StandardTestDispatcherControl coroutine execution manually; use with advanceUntilIdle()
Dispatchers.setMainReplace Main dispatcher in ViewModel tests
Turbine .test {}Assert Flow emissions in order
mockito-kotlinIdiomatic Kotlin API for Mockito
whenever(...).thenReturn(...)Stub return values on mocks
verify(mock).method(args)Assert specific calls were made on mocks

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
JUnit/Mockito/KotlinX Coroutines Test | Android System Design | Android Engineers