Unit Testing in Android
📖 Concept
Unit testing validates individual functions and classes in isolation. At the senior level, you should write testable code by design, not as an afterthought.
What to test:
- ViewModel logic: State transitions, error handling, data mapping
- Repository methods: Data source selection, caching, error transformation
- Use Cases: Business rules, filtering, validation
- Mappers/Utilities: Data transformation, formatting
What NOT to unit test:
- Android framework classes directly (Activity, Fragment) — use UI tests
- Third-party library internals (Room, Retrofit) — trust the library
- Trivial getters/setters — no logic = no value
Testing coroutines:
Use kotlinx-coroutines-test with runTest and StandardTestDispatcher or UnconfinedTestDispatcher.
Testing Flow:
Use .first() for single emission, Turbine library for multi-emission testing.
Test doubles:
- Fake: Working implementation with simplified logic (FakeRepository)
- Mock: Configured to return specific values (Mockk/Mockito)
- Stub: Returns hardcoded values
- Spy: Wraps real implementation, records calls
Google's preference: Fakes over Mocks. Fakes are real implementations that test behavior, not implementation details.
💻 Code Example
1// ViewModel test with coroutines2class TaskViewModelTest {3 // Replace main dispatcher for testing4 @get:Rule5 val mainDispatcherRule = MainDispatcherRule()67 private lateinit var viewModel: TaskViewModel8 private val fakeRepository = FakeTaskRepository()910 @Before11 fun setup() {12 viewModel = TaskViewModel(fakeRepository)13 }1415 @Test16 fun `loadTasks emits success state with tasks`() = runTest {17 // Given18 val tasks = listOf(Task("1", "Test", false))19 fakeRepository.setTasks(tasks)2021 // When — ViewModel loads on init2223 // Then — use Turbine to test Flow emissions24 viewModel.uiState.test {25 val state = awaitItem()26 assertTrue(state is TaskUiState.Success)27 assertEquals(tasks, (state as TaskUiState.Success).tasks)28 }29 }3031 @Test32 fun `toggleComplete updates task state`() = runTest {33 fakeRepository.setTasks(listOf(Task("1", "Test", false)))3435 viewModel.toggleComplete("1")36 advanceUntilIdle() // Process all coroutines3738 viewModel.uiState.test {39 val state = awaitItem() as TaskUiState.Success40 assertTrue(state.tasks.first().isComplete)41 }42 }4344 @Test45 fun `loadTasks emits error on repository failure`() = runTest {46 fakeRepository.setShouldFail(true)47 viewModel = TaskViewModel(fakeRepository) // Re-create to trigger load4849 viewModel.uiState.test {50 val state = awaitItem()51 assertTrue(state is TaskUiState.Error)52 }53 }54}5556// Fake repository — preferred over mocks57class FakeTaskRepository : TaskRepository {58 private val tasks = MutableStateFlow<List<Task>>(emptyList())59 private var shouldFail = false6061 fun setTasks(newTasks: List<Task>) { tasks.value = newTasks }62 fun setShouldFail(fail: Boolean) { shouldFail = fail }6364 override fun getTasks(): Flow<List<Task>> {65 if (shouldFail) return flow { throw IOException("Network error") }66 return tasks67 }6869 override suspend fun toggleComplete(id: String) {70 if (shouldFail) throw IOException("Network error")71 tasks.update { list ->72 list.map { if (it.id == id) it.copy(isComplete = !it.isComplete) else it }73 }74 }75}7677// MainDispatcherRule — replaces Main dispatcher in tests78class MainDispatcherRule(79 private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()80) : TestWatcher() {81 override fun starting(description: Description) {82 Dispatchers.setMain(dispatcher)83 }84 override fun finished(description: Description) {85 Dispatchers.resetMain()86 }87}
🏋️ Practice Exercise
Practice:
- Write unit tests for a ViewModel with 3 state transitions (Loading → Success → Error)
- Create a Fake repository and compare it to a Mock-based test
- Test a coroutine with delay() using runTest and advanceTimeBy()
- Test a Flow with multiple emissions using Turbine
- Achieve 80% code coverage for a Repository class
⚠️ Common Mistakes
Not replacing Dispatchers.Main in tests — causes 'Module with the Main dispatcher is missing' error
Using Mockito for everything — fakes test behavior, mocks test implementation details
Testing implementation instead of behavior — don't verify internal method call order
Not testing error paths — happy path works, but how does the app handle failures?
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Unit Testing in Android. Login to unlock this feature.