Skip to content

Expert: features and shipping · Testing Android apps · topic 40 of 50 · 35 min

Unit tests with JUnit and coroutines

Explanation

Write unit tests with JUnit: a class with functions marked @Test that use assertions such as assertEquals and assertTrue. Test ViewModels by giving them a fake repository, calling their event functions and checking the UI state. For coroutine code, use runTest from kotlinx-coroutines-test, and replace Dispatchers.Main with a test dispatcher.

A first JUnit test

JUnit is the standard testing framework for Kotlin and Java. The Android Studio template includes JUnit 4 for local tests. Each test is a function marked @Test; if an assertion fails, the test fails with a message showing what was expected and what actually happened.

kotlin
// app/src/main/.../Pricing.kt
object Pricing {
    fun totalWithGst(subtotalPaise: Long, ratePercent: Int): Long =
        subtotalPaise + subtotalPaise * ratePercent / 100
}

// app/src/test/.../PricingTest.kt
import org.junit.Assert.assertEquals
import org.junit.Test

class PricingTest {
    @Test
    fun gstAt18PercentIsAdded() {
        assertEquals(118_000L, Pricing.totalWithGst(100_000L, 18))
    }

    @Test
    fun zeroSubtotalStaysZero() {
        assertEquals(0L, Pricing.totalWithGst(0L, 18))
    }
}
assertEquals(expected, actual): the expected value comes first.

A useful pattern for every test is Arrange, Act, Assert: set up the data, do the action, check the result. Keep one behaviour per test so a failure points to exactly one problem.

Other useful assertions are assertTrue(condition), assertFalse(condition), assertNull(value) and assertNotNull(value). To check that code throws, use assertThrows (JUnit 4.13 and later) or try/catch with fail(). Many teams also use a small assertion library such as Truth for more readable checks, but plain JUnit is enough to start.

Tests for pure Kotlin code, code with no Android classes, are the easiest of all. This is another reason to keep business rules out of activities and composables: a function in a plain Kotlin object can be tested in milliseconds.

Testing a ViewModel with a fake

kotlin
interface CartRepository {
    suspend fun priceOf(item: String): Int
}

class FakeCartRepository : CartRepository {
    override suspend fun priceOf(item: String) = when (item) { "chai" -> 15; else -> 40 }
}

class CartViewModel(private val repo: CartRepository) : ViewModel() {
    private val _total = MutableStateFlow(0)
    val total: StateFlow<Int> = _total.asStateFlow()
    fun add(item: String) = viewModelScope.launch { _total.update { it + repo.priceOf(item) } }
}
The ViewModel depends on an interface, so the test can pass a fake.

runTest and the Main dispatcher

viewModelScope uses Dispatchers.Main, which does not exist in a local JVM test. The kotlinx-coroutines-test library solves this: Dispatchers.setMain replaces Main with a test dispatcher, and runTest runs coroutine code in a test, skipping delays so tests stay fast. Many teams wrap the setMain/resetMain calls in a small reusable JUnit rule (often called MainDispatcherRule).

kotlin
@OptIn(ExperimentalCoroutinesApi::class)
class CartViewModelTest {
    private val testDispatcher = UnconfinedTestDispatcher()

    @Before fun setUp() { Dispatchers.setMain(testDispatcher) }
    @After fun tearDown() { Dispatchers.resetMain() }

    @Test
    fun addingTwoChaisGivesThirty() = runTest {
        val vm = CartViewModel(FakeCartRepository())
        vm.add("chai")
        vm.add("chai")
        assertEquals(30, vm.total.value)
    }
}
Add testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:<version>") to Gradle. Check the docs for the current version.

Naming and organising

Name tests after behaviour, not methods: addingTwoChaisGivesThirty is clearer than testAdd. Some teams use backtick names in Kotlin tests, like `adding two chais gives thirty`, which JUnit on the JVM allows. Keep test files in the same package as the code they test, under src/test.

Think of it like this: A ViewModel test with a fake repository is like a driving test on a training ground: the cones (fake data) are placed exactly where the examiner wants, so every candidate faces the same situation, and nobody's result depends on traffic that day (the real network).

Common mistakes

  • Swapping expected and actual in assertEquals, which makes failure messages confusing.
  • Forgetting Dispatchers.setMain: "Module with the Main dispatcher is missing".
  • Real delays in tests. runTest skips delay() with virtual time; do not use Thread.sleep.
  • One giant test that checks ten things. Split it.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Unit tests with JUnit and coroutines · 1/6

@Test
fun gstIsAdded() {
    assertEquals(118_000L, Pricing.totalWithGst(100_000L, 18))
}

JUnit tests are functions marked Test. Assertions check results.

Prefer a video by a teacher? Find free videos on “android unit testing viewmodel junit runTest coroutines fake repository” on YouTube ↗

In real life: Testing EMI rules in a loan app

A loan app shows monthly EMIs for different tenures. The team writes unit tests with values taken from the bank's official calculator, plus edge cases: zero interest, one month, and a very large amount. The ViewModel tests use a FakeRatesRepository so they never depend on the live rates server, and the whole suite of 120 tests runs in under 3 seconds.

kotlin
class EmiTest {
    @Test
    fun zeroInterestSplitsEvenly() {
        assertEquals(1000.0, Emi.monthly(12_000.0, 0.0, 12), 0.001)
    }

    @Test
    fun knownValueMatchesBankCalculator() {
        assertEquals(10_379.18, Emi.monthly(500_000.0, 9.0, 60), 0.5)
    }
}
assertEquals for Double takes a delta (allowed difference).

Practice (0/3 done)

Task 1 · Question

In assertEquals(a, b), which argument is the expected value?

Task 2 · Question

A ViewModel test fails with "Module with the Main dispatcher is missing". What is the fix?

Task 3 · Practice

Write unit tests for a LoginViewModel that validates a 10-digit mobile number and calls a fake AuthRepository. Cover: invalid number shows an error state, valid number calls the repository and shows success, repository failure shows an error message. Use runTest and a Main dispatcher rule.

Quick quiz

Question 1

1.Which annotation marks a JUnit test function?

Question 2

2.What does runTest do with delay() calls?

Question 3

3.Why does a ViewModel take its repository as a constructor parameter?

Question 4

4.Which library provides runTest?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which annotation marks a JUnit test function?

Answer: @Test

JUnit runs every @Test function.

What does runTest do with delay() calls?

Answer: Skips them using virtual time

Tests stay fast.

Why does a ViewModel take its repository as a constructor parameter?

Answer: So tests can pass a fake

Dependency injection enables testing.

Which library provides runTest?

Answer: kotlinx-coroutines-test

Add it as a testImplementation dependency.

Which dispatcher runs on the Android UI thread?Related

Answer: Dispatchers.Main

Main is the UI thread.

From Dispatchers and withContext

Which package holds the JUnit 5 (Jupiter) annotations?Related

Answer: org.junit.jupiter.api

org.junit was JUnit 4; junit.framework was JUnit 3.

From Unit testing with JUnit

What is a coroutine?Related

Answer: A lightweight piece of work that can suspend and resume

Many coroutines can share a few threads.

From Why coroutines? suspend functions

Your code sends real SMS messages. How should a unit test handle this?Related

Answer: Pass in a fake SMS sender that only records messages

A fake (a test double) stands in for the slow or risky part, and the test checks what the code asked it to do.

From Unit testing: checking one piece at a time

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Testing Android apps” before its exam: Compose UI tests, Debugging and performance.
  2. Module 10 exam: Testing Android apps · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Publishing on the Play Store and Firebase. 50 topics left in the course before the final exam.

← Previous: Testing basics and the test pyramid