Expert: features and shipping · Testing Android apps · topic 40 of 50 · 35 min
Unit tests with JUnit and coroutines
Explanation
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.
// 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))
}
}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
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) } }
}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).
@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)
}
}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.
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.
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)
}
}Practice (0/3 done)
In assertEquals(a, b), which argument is the expected value?
A ViewModel test fails with "Module with the Main dispatcher is missing". What is the fix?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Compose UI testsTesting Android apps · same module
- Debugging and performanceTesting Android apps · same module
- Unit testing with JUnitJava Programming (Core to Spring Boot) · JDBC, Maven, Gradle and JUnit
- Unit testing: checking one piece at a timeBackend Development · Testing and API documentation
- ViewModel and MVVMNavigation and app architecture
- App architecture: layers and repositoriesNavigation and app architecture
Questions people ask about this
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
Which package holds the JUnit 5 (Jupiter) annotations?Related
Answer: org.junit.jupiter.api
org.junit was JUnit 4; junit.framework was JUnit 3.
What is a coroutine?Related
Answer: A lightweight piece of work that can suspend and resume
Many coroutines can share a few threads.
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.
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
- 2 more topics in “Testing Android apps” before its exam: Compose UI tests, Debugging and performance.
- Module 10 exam: Testing Android apps · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Publishing on the Play Store and Firebase. 50 topics left in the course before the final exam.