Skip to content
intermediate Phase 10 · Testing

Unit Testing

Write JUnit tests for ViewModels, repositories, and utility classes with Mockito or MockK.

50m
3 problems
Topic Progress 0%

JUnit and Test Structure

JUnit and Test Structure

JUnit 4 is the standard test framework for Android. Every test class follows the same pattern:

@RunWith(MockKJUnitRunner::class)
class LoginViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @MockK
    private lateinit var authRepository: AuthRepository

    private lateinit var viewModel: LoginViewModel

    @Before
    fun setup() {
        MockKAnnotations.init(this)
        viewModel = LoginViewModel(authRepository)
    }

    @Test
    fun `login with valid credentials navigates to home`() = runTest {
        coEvery { authRepository.login(any(), any()) } returns Result.success(User("test"))

        viewModel.login("test@example.com", "password")

        val state = viewModel.uiState.first()
        assertThat(state).isInstanceOf(LoginUiState.Success::class.java)
    }

    @Test
    fun `login with empty email shows error`() = runTest {
        viewModel.login("", "password")

        val state = viewModel.uiState.first()
        assertThat(state).isInstanceOf(LoginUiState.Error::class.java)
        assertThat((state as LoginUiState.Error).message).contains("email")
    }
}

Key Annotations

  • @Test — Marks a method as a test.
  • @Before — Runs before each test. Use for setup.
  • @After — Runs after each test. Use for cleanup.
  • @BeforeClass / @AfterClass — Runs once per test class. Use for expensive setup.
  • @Rule — Adds reusable test behavior (rules, watchers, dispatchers).
  • @MockK — Creates a MockK mock for a dependency.

Given-When-Then

Every test follows the Arrange-Act-Assert pattern:

  1. Given — Set up preconditions and mock behavior.
  2. When — Call the method under test.
  3. Assert — Verify the result matches expectations.

This structure makes tests readable and maintainable.

MockK Deep Dive

MockK Deep Dive

MockK is the Kotlin-native mocking library. It handles coroutines, final classes, and extension functions naturally.

Creating Mocks

// Regular mock
private val repository = mockk<UserRepository>()

// Relaxed mock (returns default values for unstubbed calls)
private val repository = mockk<UserRepository>(relaxed = true)

// Spy (real object with selective mocking)
private val repository = spyk(RealUserRepository())

Stubbing

// Suspend function
coEvery { repository.getUser("123") } returns User(name = "Alice")

// Regular function
every { repository.getCachedUser() } returns null

// Throwing an exception
every { repository.getUser("bad") } throws IllegalArgumentException("Invalid ID")

// Sequential returns
coEvery { repository.getUser(any()) } returnsMany listOf(
    User(name = "Alice"),
    User(name = "Bob")
)

Verifying Interactions

// Verify called once
coVerify(exactly = 1) { repository.getUser("123") }

// Verify never called
coVerify(exactly = 0) { repository.deleteUser(any()) }

// Verify ordering
coVerifyOrder {
    repository.getUser("123")
    repository.cacheUser(any())
}

// Verify no other interactions
coVerifyAll {
    repository.getUser("123")
    repository.cacheUser(any())
}

Mocking Flows

@Test
fun `notifications flow emits unread count`() = runTest {
    val notifications = flowOf(listOf(
        Notification(id = "1", read = false),
        Notification(id = "2", read = true)
    ))
    every { notificationRepository.getNotifications() } returns notifications

    viewModel.unreadCount.test {
        assertThat(awaitItem()).isEqualTo(1)
        cancelAndIgnoreRemainingEvents()
    }
}

Testing ViewModels

Testing ViewModels

ViewModels hold UI state and business logic. They are the most testable layer because they have no Android framework dependencies when written correctly.

ViewModel Test Pattern

@RunWith(MockKJUnitRunner::class)
class ProductListViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @MockK
    private lateinit var productRepository: ProductRepository

    private lateinit var viewModel: ProductListViewModel

    @Before
    fun setup() {
        MockKAnnotations.init(this)
        viewModel = ProductListViewModel(productRepository)
    }

    @Test
    fun `initial state is Loading`() = runTest {
        val state = viewModel.uiState.first()
        assertThat(state).isInstanceOf(ProductListUiState.Loading::class.java)
    }

    @Test
    fun `load products success shows products`() = runTest {
        coEvery { productRepository.getProducts() } returns listOf(
            Product(id = "1", name = "Widget", price = 9.99)
        )

        viewModel.loadProducts()

        val state = viewModel.uiState.first()
        assertThat(state).isInstanceOf(ProductListUiState.Success::class.java)
        assertThat((state as ProductListUiState.Success).products).hasSize(1)
    }

    @Test
    fun `load products error shows error`() = runTest {
        coEvery { productRepository.getProducts() } throws IOException("Network error")

        viewModel.loadProducts()

        val state = viewModel.uiState.first()
        assertThat(state).isInstanceOf(ProductListUiState.Error::class.java)
    }
}

Testing SavedStateHandle

ViewModels often receive parameters through SavedStateHandle:

@Test
fun `product id is extracted from saved state handle`() {
    val savedStateHandle = SavedStateHandle(mapOf("productId" to "42"))
    val viewModel = ProductDetailViewModel(savedStateHandle, productRepository)

    assertThat(viewModel.productId).isEqualTo("42")
}

Common ViewModel Test Scenarios

  1. Initial state — Verify the ViewModel starts in the correct state.
  2. Happy path — Verify success states with correct data.
  3. Error handling — Verify error states with meaningful messages.
  4. Loading states — Verify loading appears and disappears.
  5. Side effects — Verify navigation events, snackbar messages.
  6. Edge cases — Empty lists, null values, network timeouts.

Quiz

1. What is the difference between every and coEvery in MockK?

Question 1 options

2. What does a relaxed mock return for unstubbed calls?

Question 2 options

3. Why use spyk instead of mockk?

Question 3 options

4. What does coVerifyAll do that coVerify does not?

Question 4 options

Flashcards

Question

What is the difference between mockk and spyk?

Answer

mockk creates a fully fake object with no real behavior. spyk wraps a real object and lets you selectively override specific methods.

Question

How do you stub a suspend function in MockK?

Answer

Use coEvery { } instead of every { }. Example: coEvery { repo.getUser(any()) } returns User("Alice")

Question

What does MainDispatcherRule do?

Answer

It replaces Dispatchers.Main with a test dispatcher so ViewModel tests do not crash when code uses withContext(Dispatchers.Main).

Question

What are the three phases of Given-When-Then?

Answer

Given: set up preconditions and mocks. When: call the method under test. Assert: verify the result matches expectations.

Revision Notes

Key Takeaways

  • 1. Use coEvery and coVerify for suspend functions, not every and verify
  • 2. Relaxed mocks return defaults but can hide missing stubs — prefer explicit stubbing
  • 3. ViewModels are the most testable layer when they have no Android framework deps
  • 4. Always test initial state, success, error, and edge cases
  • 5. Use MainDispatcherRule to handle Dispatchers.Main in tests

Interview Tips

  • Explain the difference between mocks and spies and when to use each
  • Walk through how you would test a ViewModel that fetches data from a repository
  • Discuss how you handle coroutine testing in unit tests
  • Know the Given-When-Then pattern and why it matters for test readability

Cheat Sheet

Unit Testing Cheat Sheet

JUnit 4 Annotations:

  • @Test — test method
  • @Before — runs before each test
  • @MockK — creates a mock
  • @get:Rule — adds test rules

MockK Core:

  • mockk() — create mock
  • spyk(T()) — wrap real object
  • every { } / coEvery { } — stub
  • verify { } / coVerify { } — check interactions
  • coVerifyAll { } — no other interactions

ViewModel Testing Pattern:

  1. Mock dependencies with @MockK
  2. Set up Dispatchers with MainDispatcherRule
  3. Test initial state, happy path, error path
  4. Use Turbine or first() to collect state

Coroutine Testing:

  • runTest { } — test coroutine scope
  • coEvery / coVerify — for suspend functions
  • TestDispatcher — control timing