Testing in Kotlin (JUnit & kotlin.test)

Reviewed & published by Brayan K

Automated tests are small functions, marked with @Test, that call your code and assert the result — kotlin.test and JUnit let you verify behaviour and catch regressions before they ship.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

You'll write tests with assertEquals, assertTrue, and assertFailsWith, use readable backtick names, and follow Arrange-Act-Assert.

What You'll Learn in This Lesson

1️⃣ Your First Test: @Test and assertions

A test is a function marked @Test inside a class. Inside it you call your code and check the result with an assertion. assertEquals(expected, actual) fails if they differ; assertTrue(cond) fails if the condition is false. Backtick names make each test a readable sentence.

// A real test class with JUnit 5 + kotlin.test.
// (Runs under a test runner like Gradle, not main().)
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

fun add(a: Int, b: Int): Int = a + b

class CalculatorTest {

    // @Test marks a function the runner will execute.
    // Backtick names let you write a readable sentence.
    @Test
    fun `add returns the sum of two numbers`() {
        // assertEquals(expected, actual)
        assertEquals(5, add(2, 3))
    }

    @Test
    fun `add of a number and zero is the number`() {
        assertTrue(add(7, 0) == 7)
    }
}

2️⃣ Arrange-Act-Assert and Testing Errors

Structure each test in three phases: Arrange the inputs, Act by calling the code once, and Assert the outcome. To test that bad input fails correctly, wrap the call in assertFailsWith<Exception> { ... }, which passes only when that exact exception is thrown.

// The Arrange-Act-Assert pattern keeps tests readable.
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

fun divide(a: Int, b: Int): Int = a / b

class DivideTest {

    @Test
    fun `divides two numbers`() {
        // Arrange
        val a = 10
        val b = 2
        // Act
        val result = divide(a, b)
        // Assert
        assertEquals(5, result)
    }

    @Test
    fun `dividing by zero throws`() {
        // assertFailsWith checks the RIGHT exception is thrown.
        assertFailsWith<ArithmeticException> {
            divide(1, 0)
        }
    }
}

3️⃣ A Runnable Demo You Can Try Now

Real test classes need a build tool to run, but the idea — call a function, compare to an expected value, report pass/fail — fits in a plain main(). This little harness lets you watch assertions succeed in the Playground.

// A runnable demo of the SAME idea, using plain main()
// so you can see assertions pass/fail in the Playground.
fun add(a: Int, b: Int): Int = a + b

fun check(name: String, expected: Int, actual: Int) {
    if (expected == actual) {
        println("PASS: $name")
    } else {
        println("FAIL: $name (expected $expected, got $actual)")
    }
}

fun main() {
    check("2 + 3", 5, add(2, 3))
    check("7 + 0", 7, add(7, 0))
    check("10 + 10", 20, add(10, 10))
}

Your turn. Fill in the ___ blank, then run and compare.

// 🎯 YOUR TURN — replace each ___ then run main()
fun square(n: Int): Int = n * n

fun check(name: String, expected: Int, actual: Int) {
    // 1) print "PASS: <name>" when they match, else "FAIL: <name>"
    if (expected ___ actual) {           // 👉 put: ==
        println("PASS: $name")
    } else {
        println("FAIL: $name")
    }
}

fun main() {
    check("square 4", 16, square(4))
    check("square 5", 25, square(5))

    // ✅ Expected output:
    // PASS: square 4
    // PASS: square 5
}

Mini-Challenge: Test isEven

Write a Boolean check harness and verify isEven.

// 🎯 MINI-CHALLENGE: test a function with a mini harness.
// Save as IsEvenTest.kt and run main().
//
// 1. Write isEven(n) returning n % 2 == 0.
// 2. Write check(name, expected, actual) for Booleans that prints
//    "PASS: <name>" or "FAIL: <name>".
// 3. In main(), check isEven(4)==true and isEven(7)==false.
//
// ✅ Expected output:
//    PASS: 4 is even
//    PASS: 7 is odd

fun isEven(n: Int): Boolean = n % 2 == 0

fun check(name: String, expected: Boolean, actual: Boolean) {
    // print PASS or FAIL
}

fun main() {
    check("4 is even", true, isEven(4))
    check("7 is odd", false, isEven(7))
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — kotlin.test

AssertionPasses when
assertEquals(e, a)e == a
assertTrue(c)c is true
assertFalse(c)c is false
assertNull(x)x is null
assertFailsWith<E> { }block throws E
@Testmarks a test function

🎉 Lesson Complete!

Practice quiz

Which annotation marks a function as a test the runner will execute?

  • @Run
  • @Check
  • @Verify
  • @Test

Answer: @Test. @Test from kotlin.test (or JUnit) marks a function as a test.

In assertEquals(expected, actual), which argument comes first?

  • The expected value
  • The actual value
  • The error message
  • The test name

Answer: The expected value. Convention is assertEquals(expected, actual); expected comes first.

What does assertTrue(condition) check?

  • That two values are equal
  • That the condition is true
  • That a value is null
  • That an exception is thrown

Answer: That the condition is true. assertTrue passes only when the given Boolean condition is true.

Which assertion verifies that a block throws a specific exception?

  • assertEquals
  • assertTrue
  • assertFailsWith<T> { ... }
  • assertNull

Answer: assertFailsWith<T> { ... }. assertFailsWith<T> { ... } passes only if the block throws type T.

How does kotlin.test relate to JUnit?

  • It replaces JUnit entirely with no runner
  • It is unrelated to test runners
  • It only works on Android
  • It provides assertions and on the JVM can delegate to a runner like JUnit 5

Answer: It provides assertions and on the JVM can delegate to a runner like JUnit 5. kotlin.test is a multiplatform assertion API that can delegate to JUnit on the JVM.

Why use backtick names like `add returns the sum`() for tests?

  • They let the test name read as a sentence in reports
  • They make tests run faster
  • They are required by the compiler
  • They hide the test from the runner

Answer: They let the test name read as a sentence in reports. Backtick names allow spaces, turning test names into readable specifications.

What are the three phases of the Arrange-Act-Assert pattern?

  • Allocate, Apply, Audit
  • Arrange, Act, Assert
  • Assign, Await, Announce
  • Ask, Answer, Archive

Answer: Arrange, Act, Assert. Arrange sets up inputs, Act runs the code once, Assert checks the outcome.

What does assertFalse(condition) pass for?

  • When the condition is true
  • When the condition is false
  • When the value is null
  • Always

Answer: When the condition is false. assertFalse passes only when the Boolean condition is false.

When do you typically need a mocking library like MockK?

  • For every function including pure ones
  • Never in Kotlin
  • To replace real dependencies like databases or network clients
  • To run main()

Answer: To replace real dependencies like databases or network clients. Mock real, slow, or non-deterministic collaborators; pure functions need no mocks.

Good practice for a single test is to verify how many behaviours?

  • One behaviour per test
  • As many as possible
  • Exactly five
  • None

Answer: One behaviour per test. Each test should exercise one behaviour so a failure has a clear cause.

Continue this course

Frequently asked questions

What is kotlin.test and how does it relate to JUnit?

kotlin.test is a small, multiplatform assertion library that provides @Test plus functions like assertEquals, assertTrue, assertFalse, assertNull, and assertFailsWith. On the JVM it typically delegates to an underlying runner such as JUnit 5, so kotlin.test gives you a clean, framework-agnostic API while JUnit (or another engine) actually discovers and runs the tests. The practical upside is that the same assertions work whether you target the JVM, JavaScript, or native, and you are not locked to one framework's assertion style.

How do the common assertions work?

assertEquals(expected, actual) fails unless the two are equal — note the order is expected first, then actual, which affects the failure message. assertTrue(condition) and assertFalse(condition) check a Boolean directly. assertNull and assertNotNull check nullability. assertFailsWith<SomeException> { ... } runs the block and passes only if it throws the specified exception type, which is the idiomatic way to test error paths. Each assertion takes an optional message string you can supply to make a failure easier to diagnose.

Why use backtick test names?

Kotlin lets you name a function with backticks, allowing spaces and ordinary punctuation: fun `add returns the sum`() { }. For test methods this is hugely helpful because the test name becomes a readable sentence describing the behaviour under test, and that sentence is exactly what appears in the test report when it passes or fails. It turns a wall of camelCase names into self-documenting specifications, so a failing test immediately tells you which behaviour broke without reading the body.

What is the Arrange-Act-Assert pattern?

Arrange-Act-Assert (AAA) is a three-part structure that keeps each test focused and readable. Arrange sets up the inputs and any objects the test needs; Act performs the single operation under test, usually one method call; Assert checks the outcome with one or a few assertions. Keeping these phases visually separate (often with blank lines or comments) makes a test easy to scan and discourages testing several unrelated things at once. A good test exercises one behaviour and has a clear, single reason to fail.

What is MockK and when do I need it?

MockK is the popular Kotlin mocking library used to create fake versions of dependencies so you can test a unit in isolation. When the code under test calls out to a collaborator — a repository, a network client, a clock — you replace that collaborator with a mock whose behaviour you script (every { repo.find(1) } returns user) and whose interactions you verify (verify { repo.save(user) }). You need mocking when a real dependency is slow, non-deterministic, or has side effects you do not want in a test. For pure functions like the add() example, no mocking is required — just call and assert.