Every change you make to a program risks breaking something that used to work. Checking by hand after each change, running the app and tapping through screens, is slow and easy to skip. Automated tests check your code for you in seconds, every time.
A unit test is a small function that runs one piece of your code with known inputs and checks the result. Kotlin has its own testing library, kotlin.test, which works on top of JUnit, the standard test runner on the JVM. You mark functions with @Test, call your code, and assert what should happen. If an assertion fails, the test report shows the expected and actual values side by side.
In this lesson you'll write tests for functions and classes, check that errors are thrown correctly, share setup between tests, test suspend functions, replace slow dependencies with fakes, and run everything with Gradle. Testing is the habit that lets professionals change code confidently.
Setting up and writing a test
In a Gradle project, tests live in src/test/kotlin, next to src/main/kotlin. Add the dependency testImplementation(kotlin("test")) to build.gradle.kts; IntelliJ IDEA's Kotlin project template does this for you.
import kotlin.test.Test
import kotlin.test.assertEquals
class PriceCalculatorTest {
@Test
fun `ten percent discount on 50 gives 45`() {
assertEquals(45.0, applyDiscount(50.0, 10))
}
}@Testmarks a function as a test.- Kotlin lets you name tests with backticks, so the name can be a readable sentence describing the behaviour.
assertEquals(expected, actual): the expected value comes first.
Assertions
| Assertion | Passes when |
|---|---|
assertEquals(a, b) | a equals b |
assertNotEquals(a, b) | they differ |
assertTrue(x) / assertFalse(x) | x is true / false |
assertNull(x) / assertNotNull(x) | x is / isn't null |
assertFailsWith<T> { ... } | the block throws exception type T |
assertContains(list, x) | the collection contains x |
For Doubles, pass an allowed difference: assertEquals(0.3, 0.1 + 0.2, absoluteTolerance = 0.0001). assertFailsWith returns the exception, so you can also check its message. Data classes compare by value, so assertEquals(expectedUser, actualUser) checks every property at once.
Setup and fakes
A function marked @BeforeTest runs before each test, so every test starts fresh, for example with a new, empty shopping cart.
Real code often depends on slow or unpredictable things: a database, a web API, the current time. In tests, replace them with fakes: simple classes that implement the same interface but return fixed data. If your class takes a UserRepository interface in its constructor, a test can pass in a FakeUserRepository with three known users. This is why depending on interfaces, as in the inheritance lesson, matters so much.
Testing suspend functions
Suspend functions can't be called from a normal test function. The kotlinx-coroutines-test library provides runTest, which runs the test in a coroutine and skips delays, so a function that waits five seconds is tested instantly:
@Test
fun `loads the user after a delay`() = runTest {
assertEquals("User 7", fetchUser(7))
}Run all tests with ./gradlew test (or gradlew test on Windows), or click the green arrow beside a test in IntelliJ IDEA.
Kotlin Testing Syntax
kotlin.test essentials
import kotlin.test.*
class ThingTest {
private lateinit var thing: Thing
@BeforeTest
fun setUp() { thing = Thing() }
@Test
fun `does something useful`() {
assertEquals(42, thing.answer())
}
@Test
fun `rejects bad input`() {
assertFailsWith<IllegalArgumentException> { thing.use(-1) }
}
}Kotlin Testing Summary
- Tests live in src/test/kotlin and use kotlin.test on top of JUnit.
- @Test marks a test; backtick names describe behaviour in plain words.
- assertEquals(expected, actual), assertTrue, assertNull and assertFailsWith check results.
- @BeforeTest gives each test fresh setup; fakes replace slow dependencies through interfaces.
- runTest tests suspend functions and skips delays; run tests with gradlew test.
Kotlin Testing Examples
Each example shows the code, the output it produces and what each part does.
Testing a cart class
// src/main/kotlin/Cart.kt
class Cart {
private val prices = mutableListOf<Double>()
fun add(price: Double) {
require(price >= 0) { "Price can't be negative" }
prices.add(price)
}
val count get() = prices.size
val total get() = prices.sum()
}// src/test/kotlin/CartTest.kt
import kotlin.test.*
class CartTest {
private lateinit var cart: Cart
@BeforeTest
fun setUp() {
cart = Cart()
}
@Test
fun `empty cart has zero total`() {
assertEquals(0.0, cart.total)
}
@Test
fun `total adds every price`() {
cart.add(2.50)
cart.add(3.25)
assertEquals(2, cart.count)
assertEquals(5.75, cart.total, absoluteTolerance = 0.001)
}
@Test
fun `negative price is rejected`() {
val e = assertFailsWith<IllegalArgumentException> { cart.add(-1.0) }
assertEquals("Price can't be negative", e.message)
}
}$ ./gradlew test
CartTest > empty cart has zero total PASSED
CartTest > total adds every price PASSED
CartTest > negative price is rejected PASSED
BUILD SUCCESSFULLine-by-line explanation
- Lines 7 to 10In CartTest.kt, @BeforeTest creates a new cart before every test, so tests can't affect each other.
- Lines 21 to 22Two assertions check the count and the total; Doubles get a tolerance.
- Lines 27 to 28assertFailsWith checks the exception type and returns it, so its message can be checked too.
A fake repository
interface UserRepository {
fun findName(id: Int): String?
}
class Greeter(private val repo: UserRepository) {
fun greet(id: Int): String = repo.findName(id)?.let { "Welcome back, $it!" } ?: "Welcome, guest!"
}
class FakeUserRepository : UserRepository {
private val users = mapOf(1 to "Ada", 2 to "Linus")
override fun findName(id: Int) = users[id]
}
class GreeterTest {
private val greeter = Greeter(FakeUserRepository())
@Test
fun `known user is greeted by name`() {
assertEquals("Welcome back, Ada!", greeter.greet(1))
}
@Test
fun `unknown user is greeted as guest`() {
assertEquals("Welcome, guest!", greeter.greet(99))
}
}GreeterTest > known user is greeted by name PASSED
GreeterTest > unknown user is greeted as guest PASSEDLine-by-line explanation
- Line 5Greeter depends on the interface, not on a real database.
- Lines 9 to 12The fake returns fixed, known data instantly.
- Line 15The test gives Greeter the fake instead of a real repository.
Testing a suspend function
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import kotlin.test.*
suspend fun fetchPrice(item: String): Double {
delay(5_000)
return if (item == "tea") 2.5 else error("Unknown item: $item")
}
class PriceTest {
@Test
fun `tea costs 2 point 5`() = runTest {
assertEquals(2.5, fetchPrice("tea"))
}
@Test
fun `unknown items fail`() = runTest {
assertFailsWith<IllegalStateException> { fetchPrice("pizza") }
}
}PriceTest > tea costs 2 point 5 PASSED
PriceTest > unknown items fail PASSED
(both tests finish in milliseconds: runTest skips the 5 second delay)Line-by-line explanation
- Line 6A five-second delay that would make tests painfully slow.
- Line 12runTest runs the test in a coroutine with virtual time, so delays are skipped.
- Line 18assertFailsWith works inside runTest too.
Common Kotlin Testing Mistakes
Expected and actual the wrong way round
assertEquals(cart.total, 5.75) // failure messages read backwardsassertEquals(5.75, cart.total, absoluteTolerance = 0.001)Tests that share state
companion object { val cart = Cart() } // one test's items leak into the nextprivate lateinit var cart: Cart
@BeforeTest fun setUp() { cart = Cart() }Calling the real network in a unit test
val greeter = Greeter(ApiUserRepository("https://api.example.com")) // slow and flakyval greeter = Greeter(FakeUserRepository())Kotlin Testing in Real Projects
Testing in Kotlin teams
Android and server teams run thousands of Kotlin unit tests on every pull request through continuous integration such as GitHub Actions. Libraries like MockK create fakes automatically, Turbine tests flows, and Kotest offers an alternative, more expressive test style. Code designed around interfaces and small functions is much easier to test, which is one reason experienced developers care so much about that design.
Kotlin Testing FAQ
What is the difference between kotlin.test and JUnit?
kotlin.test is a small set of annotations and assertion functions, such as @Test and assertEquals, that work the same on every platform Kotlin targets. On the JVM it runs on top of a real test framework, usually JUnit 5, which does the actual discovery and running of tests. You can also use JUnit's own annotations and assertions directly; many projects mix the two.
How do I run a single test class with Gradle?
Pass a filter to the test task: ./gradlew test --tests "PriceCalculatorTest" runs only that class, and wildcards such as --tests "*Cart*" match by pattern. In IntelliJ IDEA, the green arrow beside a class or a single test function runs just that one.
Should I test private functions in Kotlin?
Usually not directly. Test the public functions that use them, because that's the behaviour other code relies on, and it leaves you free to restructure private code. If a private function is complex enough to need its own tests, consider moving it into a separate class, or making it internal, which is visible to tests in the same module.
What is the difference between a fake and a mock?
A fake is a small, working implementation you write yourself, such as an in-memory repository backed by a map. A mock is generated by a library such as MockK and is programmed per test to return values and to record which calls were made. Fakes are simpler to read and reuse; mocks help when you need to check that a particular call happened.
Why does my test fail when comparing Double values?
Many decimals can't be stored exactly in binary, so 0.1 + 0.2 is 0.30000000000000004 and assertEquals(0.3, 0.1 + 0.2) fails. Pass a tolerance instead: assertEquals(0.3, 0.1 + 0.2, absoluteTolerance = 0.0001). For money, storing whole pence as Long or using BigDecimal avoids the problem altogether.
Kotlin Testing Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the annotation that marks a function as a test.
fun `adds two numbers`() {
assertEquals(4, 2 + 2)
}
In assertEquals(a, b), which is the expected value?
How do you check that a call throws IllegalArgumentException?
Why use a fake repository in tests?
What does runTest do with delay(5_000)?
Kotlin Testing Quiz
5 questions to check what you learned.
Where do test files usually live in a Gradle Kotlin project?
- src/main/kotlin
- tests/
- build/
- src/test/kotlin
What does @BeforeTest do?
- Runs before each test, giving it fresh setup
- Runs once before all tests
- Skips the next test
- Runs only when a test fails
Why can Kotlin test names contain spaces?
- They can't
- Only on Windows
- Names written in backticks can contain spaces, so tests read like sentences
- Only with JUnit 4
How do you compare Doubles safely in a test?
- Convert them to Strings
- Pass absoluteTolerance to assertEquals
- Round both to Ints
- Doubles can't be tested
Which command runs all tests in a Gradle project?
- kotlin test
- ./gradlew test
- gradle run
- java -test