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.

Note: Every test class and assertion shown here is correct, real JUnit/MockK code as it would run inside an actual Android Studio project — but this website's own project doesn't run a live Android Studio build, so nothing here executes on the page itself.
Up next: UI Testing (Espresso) moves one layer up the testing pyramid, checking that TrailLog's actual screens behave correctly when a real user taps through them.