Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
157: Testing Android ViewModels
Imagine you're testing a high-end remote control for a smart TV. To make sure the remote works, you don't actually need the TV plugged into a wall, a screen glowing, or a cable subscription active. You just need a way to "sniff" the infrared signal the remote sends. If you press the "Volume Up" button and the remote sends the exact binary code for VOLUME_INCREASE, the remote is doing its job. The TV's ability to actually make the room louder is a separate problem for the TV engineers.
Testing a ViewModel is exactly the same. The ViewModel is your remote. The Activity or Fragment is the TV. I've seen too many developers try to test their logic by launching an emulator and clicking buttons. That's slow and brittle. Instead, we want to trigger a function in the ViewModel and then "sniff" the resulting state—usually a StateFlow or LiveData—to see if it changed to what we expected.
The Main Dispatcher Headache
Before we get to the code, we have to address the elephant in the room: Dispatchers.Main. Your ViewModel likely uses viewModelScope, which is hard-wired to the Android Main thread. But JUnit tests run on a JVM, and the JVM has no idea what an "Android Main thread" is. If you try to run a ViewModel test without a fix, it'll crash immediately with a RuntimeException telling you the Main looper is not mocked.
I usually handle this by creating a JUnit Rule or a helper function that swaps Dispatchers.Main for a TestDispatcher. This tricks the ViewModel into thinking it's on the main thread while it's actually running on your test thread.
Putting the WeatherViewModel to the Test
Let's look at a real scenario. Suppose we have a WeatherViewModel that fetches the temperature for a city. It has a UIState that can be Loading, Success, or Error.
class WeatherViewModel(private val repository: WeatherRepository) : ViewModel() {
private val _uiState = MutableStateFlow<WeatherState>(WeatherState.Idle)
val uiState: StateFlow<WeatherState> = _uiState
fun fetchWeather(city: String) {
viewModelScope.launch {
_uiState.value = WeatherState.Loading
try {
val temp = repository.getTemperature(city)
_uiState.value = WeatherState.Success(temp)
} catch (e: Exception) {
_uiState.value = WeatherState.Error("Could not fetch weather")
}
}
}
}
To test this, we don't want to call a real API. We'll mock the WeatherRepository. I prefer using kotlinx-coroutines-test to control time. By using runTest, we can execute coroutines instantly without adding Thread.sleep() (which is a cardinal sin in unit testing).
class WeatherViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private lateinit var repository: WeatherRepository
private lateinit var viewModel: WeatherViewModel
@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
repository = mockk() // Using MockK for this example
viewModel = WeatherViewModel(repository)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `fetchWeather updates state to Success when repository returns data`() = runTest {
// Arrange: Tell the mock repository what to return
coEvery { repository.getTemperature("London") } returns 22
// Act: Trigger the action
viewModel.fetchWeather("London")
// Fast-forward the coroutine execution
advanceUntilIdle()
// Assert: Check if the state is what we expect
assertEquals(WeatherState.Success(22), viewModel.uiState.value)
}
}
The nuance of StateFlow timing
One thing that trips people up is that StateFlow is asynchronous. If you call viewModel.fetchWeather() and immediately check the value on the next line, you might still see WeatherState.Idle because the coroutine hasn't actually started executing yet. That's why I used advanceUntilIdle(). It tells the test dispatcher, "Run everything currently pending in the queue before you move to the assertion." Without it, your tests will flake or fail randomly.
📋 Practical Task
Validating the Task Addition Flow in TaskViewModel
You are building a Todo app. You have a TaskViewModel with a StateFlow<List<Task>> and a function addTask(name: String). The function should ignore empty strings and only add tasks that have a name longer than 3 characters.
Your task: Write a JUnit test class for TaskViewModel. You must implement two specific test cases:
- Successful Addition: Verify that when
addTask("Buy Milk")is called, the list in theStateFlowincreases in size by one. - Validation Failure: Verify that when
addTask("Hi")(a string too short) is called, the list size remains zero.
Ensure you use Dispatchers.setMain in your setup and runTest for your test methods to handle the viewModelScope correctly.
There are no comments for now.