This episode masters testing and code quality in Kotlin: unit testing with Kotlin Test, JUnit 5, and Kotest, mocking with MockK or Mockito, Behavior-Driven Testing patterns, and continuous testing and automation that keep your confidence in the code high.

Code that isn't tested is just speculation. Episode 13 equips you with testing and quality skills in Kotlin: unit testing with standard frameworks, mocking dependencies, Behavior-Driven Testing patterns, and automation that runs tests continuously.
The Kotlin testing ecosystem is rich: JUnit 5 as the foundation, Kotlin Test for idiomatic assertions, and Kotest for BDD and property-based testing styles. MockK is designed specifically for Kotlin, with coroutine support.
After this episode, you'll write tests that provide confidence — not just chase coverage numbers.
JUnit 5 is the de facto standard for JVM tests. With Kotlin, the @Test annotation is used directly on test functions:
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class KalkulatorTest {
private val kalkulator = Kalkulator()
@Test
fun menambahkanDuaAngka() {
assertEquals(7, kalkulator.tambah(3, 4))
}
}Test function names can use spaces when wrapped in backticks, producing names that read like sentences. assertEquals is a Kotlin Test assertion that integrates with the JUnit framework and wraps failures in informative messages.
Kotest offers several spec styles and property-based testing. The StringSpec style writes tests as a description and a lambda:
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class GajiTest : StringSpec({
"gaji bersih mengurangi pajak" {
gajiBersih(10_000_000) shouldBe 8_500_000
}
})shouldBe is a matcher that reads naturally. Kotest also has forAll for property-based testing: you define a property that must always hold, and Kotest generates many inputs to test it automatically.
MockK is a mocking library that understands Kotlin: it supports final classes, coroutines, and extension functions — things that are hard with older Java frameworks. Creating a mock and verifying interactions:
import io.mockk.*
import kotlin.test.assertTrue
class PesanServiceTest {
private val repo = mockk<PesanRepository>()
private val service = PesanService(repo)
@Test
fun simpanMemanggilRepository() {
every { repo.simpan(any()) } returns true
val hasil = service.kirim("halo")
verify { repo.simpan("halo") }
assertTrue(hasil)
}
}mockk<PesanRepository>() creates a mock without the real class. every { ... } returns ... defines behavior, and verify { ... } ensures the interaction actually happened. MockK also provides coEvery and coVerify specifically for suspend functions.
Mockito is still widely used, especially in codebases already based on Java. With the inline mock maker module, Mockito can mock final classes and Kotlin functions. Mockito's strengths are its vast ecosystem and documentation; MockK's strength is native support for Kotlin features like data classes, coroutines, and companion objects.
Behavior-Driven Testing emphasizes system behavior from the user's perspective, not implementation details. Kotest supports this style with FunSpec or DescribeSpec:
import io.kotest.core.spec.style.DescribeSpec
class KeranjangTest : DescribeSpec({
describe("keranjang belanja") {
it("menghitung total") {
val keranjang = Keranjang()
keranjang.tambah("kopi", 15000)
keranjang.tambah("teh", 8000)
keranjang.total() shouldBe 23000
}
}
})The describe and it structure makes tests read like feature specifications. These names appear in test reports, so anyone can understand what is guaranteed without reading the implementation.
The TDD principle that accompanies BDD: write a failing test first, make it pass as minimally as possible, then refactor. This cycle keeps tests focused on behavior and prevents "blind" tests that simply mirror the implementation.
All of a project's tests run with a single Gradle command:
./gradlew test
./gradlew test --tests "id.devnull.KalkulatorTest"./gradlew test runs all tests in the project, and --tests filters to specific ones. Gradle parallelizes test execution and stores reports in build/reports/tests.
To keep quality high, run tests on every code change. A common pipeline pattern:
steps:
- uses: actions/setup-java
with:
distribution: temurin
java-version: "21"
- run: ./gradlew testThe YAML block above runs tests with JDK Temurin 21 in GitHub Actions. This automation keeps quality continuously high — any pull request that breaks a test is caught before merge. Episode 19 will cover a complete pipeline with deployment.
Episode 13 equipped you with testing and quality in Kotlin: unit tests with JUnit 5 and Kotest, mocking with MockK or Mockito, behavior-centered BDD patterns, and continuous testing through Gradle and CI pipelines.
The key takeaways:
./gradlew test runs the whole suite and produces reports.In episode 14 we'll discuss build tools and project configuration — the Gradle Kotlin DSL and build scripts, dependency management with source sets and plugins, multi-module projects and build performance, and publishing artifacts and versioning.