Bird
Raised Fist0
Android Kotlinmobile~20 mins

ViewModel testing in Android Kotlin - Mini App: Build & Ship

Choose your learning style10 modes available
Build: CounterViewModelTest
Test a simple ViewModel that holds a counter value and increments it.
Target UI
-------------------
| Counter: 0      |
| [Increment]     |
-------------------
Create a ViewModel with a LiveData<Int> counter starting at 0
Add a function to increment the counter
Write a unit test to verify the initial counter value is 0
Write a unit test to verify the counter increments correctly
Starter Code
Android Kotlin
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class CounterViewModel : ViewModel() {
    private val _counter = MutableLiveData<Int>(0)
    val counter: LiveData<Int> = _counter

    // TODO: Add increment function here
}
Task 1
Task 2
Task 3
Task 4
Solution
Android Kotlin
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class CounterViewModel : ViewModel() {
    private val _counter = MutableLiveData<Int>(0)
    val counter: LiveData<Int> = _counter

    fun increment() {
        val current = _counter.value ?: 0
        _counter.value = current + 1
    }
}

// Test file
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify

class CounterViewModelTest {
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    private lateinit var viewModel: CounterViewModel
    private lateinit var observer: Observer<Int>

    @Before
    fun setup() {
        viewModel = CounterViewModel()
        observer = mock(Observer::class.java) as Observer<Int>
        viewModel.counter.observeForever(observer)
    }

    @Test
    fun initialCounterIsZero() {
        assertEquals(0, viewModel.counter.value)
    }

    @Test
    fun incrementIncreasesCounter() {
        viewModel.increment()
        assertEquals(1, viewModel.counter.value)
        viewModel.increment()
        assertEquals(2, viewModel.counter.value)
    }
}

This ViewModel holds a counter as LiveData starting at 0. The increment() function increases the counter by 1 safely handling null values. The unit test uses InstantTaskExecutorRule to run LiveData synchronously. It observes the LiveData with a mocked observer to avoid leaks. The tests check the initial value is zero and that calling increment() updates the counter correctly.

Final Result
Completed Screen
-------------------
| Counter: 0      |
| [Increment]     |
-------------------
When user taps Increment button, counter value increases by 1
UI updates to show new counter value
Stretch Goal
Add a decrement function and test it decreases the counter but never below zero
💡 Hint
Check current counter value before decrementing and write a test to verify it does not go below zero

Practice

(1/5)
1.

What is the main purpose of testing a ViewModel in Android development?

easy
A. To validate user input directly on the screen
B. To verify the data handling and business logic without UI involvement
C. To test the navigation between different screens
D. To check the layout and visual appearance of the app

Solution

  1. Step 1: Understand ViewModel role

    The ViewModel holds and manages UI-related data but does not handle UI rendering.
  2. Step 2: Identify testing focus

    Testing ViewModel focuses on data logic and state management, not UI or navigation.
  3. Final Answer:

    To verify the data handling and business logic without UI involvement -> Option B
  4. Quick Check:

    ViewModel testing = data logic check [OK]
Hint: ViewModel tests focus on data, not UI or navigation [OK]
Common Mistakes:
  • Confusing ViewModel testing with UI testing
  • Thinking ViewModel tests check screen layouts
  • Assuming navigation is tested in ViewModel tests
2.

Which annotation is commonly used to set up a ViewModel instance before each test method runs?

easy
A. @Ignore
B. @Test
C. @After
D. @Before

Solution

  1. Step 1: Recall JUnit lifecycle annotations

    @Before runs before each test method to prepare test environment.
  2. Step 2: Match annotation to ViewModel setup

    Use @Before to initialize ViewModel before tests run.
  3. Final Answer:

    @Before -> Option D
  4. Quick Check:

    Setup before tests = @Before [OK]
Hint: Use @Before to prepare ViewModel before each test [OK]
Common Mistakes:
  • Using @Test instead of @Before for setup
  • Confusing @After with setup annotation
  • Skipping setup annotation entirely
3.

Given the following test code snippet, what will be the value of result after running the test?

class MyViewModelTest {
  private lateinit var viewModel: MyViewModel

  @Before
  fun setup() {
    viewModel = MyViewModel()
  }

  @Test
  fun testIncrement() {
    viewModel.increment()
    val result = viewModel.counter
  }
}

Assuming counter starts at 0 and increment() adds 1.

medium
A. 1
B. 0
C. null
D. Throws an exception

Solution

  1. Step 1: Understand initial state

    ViewModel's counter starts at 0 before increment.
  2. Step 2: Apply increment method

    Calling increment() adds 1, so counter becomes 1.
  3. Final Answer:

    1 -> Option A
  4. Quick Check:

    Initial 0 + 1 increment = 1 [OK]
Hint: Increment adds 1 to counter starting at 0 [OK]
Common Mistakes:
  • Assuming counter remains 0 after increment
  • Thinking result is null without initialization
  • Expecting an exception without error in code
4.

Identify the error in this ViewModel test code snippet:

class SampleViewModelTest {
  private lateinit var viewModel: SampleViewModel

  @Test
  fun testReset() {
    viewModel.reset()
    assertEquals(0, viewModel.counter)
  }
}

Assuming viewModel is not initialized anywhere else.

medium
A. Missing @Before setup to initialize viewModel before test
B. Incorrect assertEquals parameter order
C. reset() method does not exist in ViewModel
D. Test method missing @Test annotation

Solution

  1. Step 1: Check viewModel initialization

    viewModel is declared but never initialized before test runs.
  2. Step 2: Identify missing setup

    Without @Before method to initialize viewModel, calling reset() causes error.
  3. Final Answer:

    Missing @Before setup to initialize viewModel before test -> Option A
  4. Quick Check:

    Initialize viewModel before test = @Before needed [OK]
Hint: Always initialize ViewModel before tests with @Before [OK]
Common Mistakes:
  • Forgetting to initialize viewModel before use
  • Mixing assertEquals parameter order
  • Assuming reset() method is missing without checking
5.

You want to test a ViewModel method that fetches data asynchronously and updates a LiveData property. Which approach is best to test this behavior?

hard
A. Mock the LiveData property and verify it was called
B. Call the method and immediately check LiveData value without waiting
C. Use a coroutine test rule to run suspend functions and observe LiveData changes synchronously
D. Test only the UI observing the LiveData, not the ViewModel method

Solution

  1. Step 1: Understand asynchronous data fetching

    ViewModel uses coroutines to fetch data and update LiveData asynchronously.
  2. Step 2: Use coroutine test tools

    Coroutine test rules allow running suspend functions in tests synchronously for reliable assertions.
  3. Step 3: Observe LiveData changes properly

    Observing LiveData synchronously ensures test waits for updates before asserting values.
  4. Final Answer:

    Use a coroutine test rule to run suspend functions and observe LiveData changes synchronously -> Option C
  5. Quick Check:

    Async ViewModel test = coroutine test rule + LiveData observe [OK]
Hint: Use coroutine test rule to handle async LiveData updates [OK]
Common Mistakes:
  • Checking LiveData immediately without waiting
  • Mocking LiveData instead of testing real updates
  • Testing UI instead of ViewModel logic
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard