Testing in Kotlin
JUnit 5 in Kotlin, backtick test names, Kotlin assertion style, and testing with a hand-written fake dependency.
Setting up JUnit 5 in Kotlin
JUnit 5 is the standard testing framework on the JVM, and Kotlin uses it directly — there's no separate "Kotlin testing framework" needed for ordinary unit tests, though a few small Kotlin-specific libraries (covered below) make assertions read more naturally than calling JUnit's own Java-style assertion methods directly.
Gradle (build.gradle.kts), the build tool almost every Kotlin project uses:
dependencies {
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
tasks.test {
useJUnitPlatform()
}
kotlin("test") pulls in Kotlin's own thin wrapper around JUnit, giving you Kotlin-idiomatic top-level assertion functions (assertEquals, assertTrue, assertFailsWith) instead of calling static Java methods directly — a small but genuine readability improvement in Kotlin code.
Writing a test class
A JUnit 5 test in Kotlin is an ordinary class, with each test a function annotated @Test:
import kotlin.test.Test
import kotlin.test.assertEquals
class InvoiceCalculatorTest {
@Test
fun `calculates subtotal without tax`() { // backtick-quoted function names allow spaces — very readable test names
val calculator = InvoiceCalculator()
val result = calculator.calculateTotal(price = 100.0, quantity = 2, taxRate = 0.0)
assertEquals(200.0, result)
}
@Test
fun `applies tax rate correctly`() {
val calculator = InvoiceCalculator()
val result = calculator.calculateTotal(price = 100.0, quantity = 1, taxRate = 0.1)
assertEquals(110.0, result)
}
}
class InvoiceCalculator {
fun calculateTotal(price: Double, quantity: Int, taxRate: Double): Double {
val subtotal = price * quantity
return subtotal + (subtotal * taxRate)
}
}
Backtick-quoted function names (`calculates subtotal without tax`) are a Kotlin-specific feature used constantly in tests — the resulting name reads like a plain sentence in test output and reports, rather than the camelCaseOrSnakeCaseRunTogether names Java's identifier rules would otherwise force.
Kotlin assertion style
Kotlin's kotlin.test package provides top-level functions rather than requiring a this.assert... call inherited from a base test class — a small difference from PHPUnit's or JUnit's Java-side style, but one that fits Kotlin's general preference for free functions over inheritance-based APIs:
import kotlin.test.*
class ShoppingCartTest {
@Test
fun `new cart starts empty`() {
val cart = ShoppingCart()
assertTrue(cart.items.isEmpty())
assertEquals(0, cart.items.size)
}
@Test
fun `adding an item increases the count`() {
val cart = ShoppingCart()
cart.add("Widget", 9.99)
assertEquals(1, cart.items.size)
assertFalse(cart.items.isEmpty())
}
@Test
fun `withdrawing more than the balance throws`() {
val account = BankAccount(50.0)
val exception = assertFailsWith<IllegalArgumentException> { // Kotlin-idiomatic exception assertion
account.withdraw(100.0)
}
assertEquals("Insufficient funds", exception.message)
}
}
assertFailsWith<ExceptionType> { ... } is the idiomatic Kotlin equivalent of PHPUnit's expectException or Java JUnit's assertThrows — it runs the block, asserts that exactly the given exception type was thrown, and returns the caught exception itself so you can go on to assert against its message or properties, all in one expression rather than a separate arm-and-verify step.
Setup with a constructor or @BeforeEach
Kotlin test classes commonly use a plain property initialized directly, since a fresh instance of the test class itself is created for each test method by default in JUnit 5 — but @BeforeEach is available and behaves identically to JUnit in Java, for cases needing more explicit setup logic:
import kotlin.test.*
import org.junit.jupiter.api.BeforeEach
class ShoppingCartTest2 {
private lateinit var cart: ShoppingCart
@BeforeEach
fun setUp() {
cart = ShoppingCart() // fresh instance before EACH test method
}
@Test
fun `starts empty`() {
assertTrue(cart.items.isEmpty())
}
@Test
fun `add increases size`() {
cart.add("Widget", 9.99)
assertEquals(1, cart.items.size)
}
}
lateinit var (covered briefly on the syntax page) is the idiomatic way to declare a property that will definitely be assigned before use (here, in @BeforeEach) but can't be assigned at declaration time — using a nullable var cart: ShoppingCart? = null instead would work but would force every test to unwrap it with !! or ?. before using it, which defeats the purpose of setup running first.
A complete example: testing a class with a fake dependency
import kotlin.test.*
interface PaymentGateway {
fun charge(amount: Double): Boolean
}
class FakePaymentGateway(private val shouldSucceed: Boolean) : PaymentGateway {
var chargeCallCount = 0
private set
override fun charge(amount: Double): Boolean {
chargeCallCount++
return shouldSucceed
}
}
class OrderProcessor(private val gateway: PaymentGateway) {
fun completeOrder(amount: Double): String {
if (!gateway.charge(amount)) {
throw IllegalStateException("Payment failed")
}
return "Order completed"
}
}
class OrderProcessorTest {
@Test
fun `completes order when payment succeeds`() {
val gateway = FakePaymentGateway(shouldSucceed = true)
val processor = OrderProcessor(gateway)
val result = processor.completeOrder(49.99)
assertEquals("Order completed", result)
assertEquals(1, gateway.chargeCallCount) // verifies HOW the dependency was used, not just the return value
}
@Test
fun `throws when payment fails`() {
val gateway = FakePaymentGateway(shouldSucceed = false)
val processor = OrderProcessor(gateway)
assertFailsWith<IllegalStateException> {
processor.completeOrder(49.99)
}
}
}
Rather than a mocking library, this uses a fake — a small, hand-written class implementing the same PaymentGateway interface the real production gateway would — which is a common, idiomatic Kotlin testing style precisely because Kotlin's concise class syntax makes writing a purpose-built fake almost as little code as configuring a general-purpose mocking library would take. (Libraries like MockK exist for more elaborate mocking needs, but a simple fake, as shown here, is often clearer and just as effective for straightforward cases.) Exposing chargeCallCount with a private setter lets the test assert on how the dependency was actually used — not just what completeOrder returned — the same verification a PHPUnit mock's expects($this->once()) provides, expressed here as an ordinary, explicit property instead.
Common mistakes
- Writing test function names in
camelCaseout of habit instead of using Kotlin's backtick-quoted names — a small thing, but readable, sentence-like test names are one of the genuine everyday conveniences Kotlin adds over Java for tests specifically. - Declaring a fixture with a nullable
var x: Foo? = nulland unwrapping it with!!in every test, instead of usinglateinit var—lateinitcommunicates "this will definitely be assigned before use" directly, and avoids sprinkling non-null assertions through every test method. - Reaching for a full mocking library for a simple interface when a small, hand-written fake class is just as easy to write in Kotlin and often easier to read, with no mocking-library-specific syntax to learn.
- Sharing one instance of a fixture across multiple test methods (declaring it once at the class level and reusing it, rather than rebuilding it in
@BeforeEachor a fresh property per test) — this risks one test's mutations leaking into and affecting another test.
Interview questions
Q: What does assertFailsWith<ExceptionType> { ... } do, and how is it more useful than just checking that a call throws something?
It runs the given block, asserts that it throws an exception of exactly the specified type (failing the test if nothing is thrown, or if a different, unrelated exception type is thrown instead), and returns the caught exception — letting you chain further assertions against its message or properties in the same expression. This gives a stronger, more specific guarantee than a bare "something was thrown," and mirrors the same idea as PHPUnit's expectException or Java JUnit's assertThrows.
Q: Why would you write a small hand-written fake implementing an interface instead of reaching for a mocking library in a Kotlin test? Kotlin's concise class syntax means implementing a simple interface with a few tracked properties (like a call counter) is often barely more code than configuring an equivalent mock through a mocking library's API, while staying fully readable as plain Kotlin with no library-specific DSL to learn. A fake also behaves like real, ordinary code — no hidden proxying or bytecode generation — which makes it easier to reason about and debug when a test fails unexpectedly. Mocking libraries like MockK still earn their place for more elaborate scenarios (verifying complex call sequences, partial mocking of large classes), but a fake is frequently the simpler, more idiomatic first choice.