Unit Testing (JUnit & MockK)
A unit test checks one small piece of pure logic in isolation — no emulator, no physical device, no real database or network call involved — which is exactly what makes unit tests so fast to run. TrailLog has plenty of logic worth testing this way: the rule that decides whether a trail counts as easy, moderate, or hard based on its distance, or a ViewModel deciding what to show while trails are loading. This lesson covers testing both kinds with the two libraries that do the heavy lifting: JUnit and MockK.
The Testing Pyramid, Briefly
Unit tests sit at the base of what's commonly called the testing pyramid: many fast, cheap, narrowly focused tests at the bottom, fewer broader UI tests above them (covered next in UI Testing (Espresso)), and fewer still full end-to-end tests at the top. The shape matters because it's inverted from the effort each layer takes to write — unit tests are the cheapest to write and run, so a healthy test suite leans heavily on them.
JUnit: @Test, assertEquals, assertTrue
JUnit is the standard test framework a unit test builds on — a function annotated
@Test is a single test case, and assertion functions like assertEquals and
assertTrue check that a computed value matches what's expected. Here's a test for
TrailLog's difficulty-rating logic:
TrailDifficulty.kt
fun rateDifficulty(distanceKm: Double): String = when {
distanceKm < 5.0 -> "Easy"
distanceKm < 15.0 -> "Moderate"
else -> "Hard"
}
TrailDifficultyTest.kt
class TrailDifficultyTest {
@Test
fun `a three kilometer trail rates as easy`() {
val result = rateDifficulty(3.0)
assertEquals("Easy", result)
}
@Test
fun `a twenty kilometer trail rates as hard`() {
val result = rateDifficulty(20.0)
assertTrue(result == "Hard")
}
}
Both tests run on the plain JVM in milliseconds — no device or emulator needed — because
rateDifficulty has no Android framework dependency at all, only a Double in
and a String out.
MockK: Mocking Built For Kotlin
Real logic worth testing usually isn't this isolated, though — TrailListViewModel depends
on a TrailRepository, and a genuine unit test shouldn't require a real Room database or
network call just to check the ViewModel's own logic. MockK is a mocking library built
specifically for Kotlin, standing in for that real TrailRepository with a fake one whose
behavior the test controls directly. It's worth contrasting with Mockito, the older standard on the
Java side: Mockito was designed before Kotlin existed, and runs into real friction with Kotlin classes
being final by default, often requiring extra configuration just to mock an ordinary
class. MockK was designed against Kotlin's actual language features from day one, so mocking a plain
Kotlin class works without workarounds.
TrailListViewModelTest.kt
class TrailListViewModelTest {
private val repository = mockk<TrailRepository>()
@Test
fun `loadTrails populates trails from the repository`() = runTest {
coEvery { repository.getTrails() } returns listOf(sampleTrail)
val viewModel = TrailListViewModel(repository)
viewModel.loadTrails()
assertEquals(listOf(sampleTrail), viewModel.trails.value)
coVerify { repository.getTrails() }
}
}
mockk<TrailRepository>() creates a fake TrailRepository with no real
database or network behavior at all — every call on it does nothing until told what to do.
coEvery and coVerify: Mocking Suspend Functions
TrailRepository.getTrails() is a suspend function, and MockK's plain
every { }/verify { } only work with ordinary, non-suspend functions. MockK's
coroutine-aware counterparts, coEvery { } and coVerify { }, exist specifically
to stub and verify suspend function calls: coEvery { repository.getTrails() } returns
listOf(sampleTrail) tells the fake exactly what to hand back when that suspend function is
called, and coVerify { repository.getTrails() } confirms the ViewModel actually called it
during the test.
Testing Coroutine Code With runTest { }
viewModel.loadTrails() internally launches a coroutine on viewModelScope, so
the test itself needs to run inside a coroutine that can wait for that launched work to actually
finish before asserting on the result. runTest { }, from
kotlinx-coroutines-test, is exactly that test-friendly coroutine builder — it runs
suspending code and automatically skips real delays, so a test that would otherwise wait on real time
completes instantly. This lesson only needs enough of runTest { } to make the example
above work; Testing Coroutines & Flow later in this
track covers runTest, virtual time, and testing Flow emissions in full depth.