

Unit Testing in Kotlin

Introduction
This guide introduces unit testing in Kotlin, with a focus on Java developers transitioning to Kotlin for backend development. Kotlin, being interoperable with Java, allows developers to leverage familiar tools while benefiting from its concise syntax and modern features. This document covers essential testing frameworks, mocking libraries, and assertion tools tailored for Kotlin, helping you write robust and readable tests.
Testing Frameworks
JUnit 5 (Jupiter)
JUnit 5 is a widely adopted testing framework in the Java ecosystem and works seamlessly with Kotlin. It provides a familiar structure with annotations like @Test
, @BeforeEach
, and @AfterEach
, making it an easy starting point for Java developers. Additionally, using @ParameterizedTest
in JUnit 5 allows you to run the same test logic with multiple input sets, streamlining data-driven testing in Kotlin with concise, reusable code.
Example: Basic Unit Test with JUnit 5
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals
class CalculatorTest {
@Test
fun `should return 4 when adding 2 and 2`() {
val calculator = Calculator()
val result = calculator.add(2, 2)
assertEquals(4, result)
}
}
💡 Tip: Kotlin allows backtick-escaped function names (e.g., `should return 4 when adding 2 and 2`), enabling descriptive, sentence-like test names for better readability. See Kotlin Coding Conventions.
Example: Test suspend function
Using kotlinx-coroutines-test
import org.junit.jupiter.api.Test
import kotlinx.coroutines.test.runTest
class MyServiceTest {
@Test
fun `test suspend function`() = runTest {
val myService = MyService()
val result = myService.fetchDataAsync()
// verifying
}
}
Kotest
Kotest is a powerful, Kotlin-native testing framework that offers a modern alternative to JUnit. While its structure might feel unfamiliar to Java developers accustomed to traditional annotations, it resembles testing styles in JavaScript (e.g., Mocha) or Ruby (e.g., RSpec). Kotest supports multiple testing styles, such as FunSpec, DescribeSpec, and BehaviorSpec, allowing flexibility based on your preferences.
Setup
Refer to the Kotest Quickstart Guide for setup instructions using Gradle or Maven.
Note: You need to install the Kotest plugin in IntelliJ IDEA to run tests directly from the editor.
Example: Using DescribeSpec
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
class ScoreTest : DescribeSpec({
describe("calculator functionality") {
it("should return 4 when adding 2 and 2") {
val calculator = Calculator()
val result = calculator.add(2, 2)
result shouldBe 4
}
}
})
Key Features
Integrates with JUnit 5 runner for compatibility with existing tools.
Handles coroutines seamlessly without additional setup, making it more convenient than JUnit 5 for coroutine testing.
Offers advanced features like property-based testing and built-in assertions (covered later).
Mocking Frameworks
Mocking is essential for isolating units of code during testing. Below are two prominent mocking libraries for Kotlin:
Comparison Table
Framework | Popularity | Kotlin-Native | Notes |
---|---|---|---|
MockK | ⭐⭐⭐⭐⭐ | ✅ | Best for Kotlin: coroutines, extension functions, and singleton support. |
Mockito-Kotlin | ⭐⭐⭐⭐ | ❌ (Java-based) | Popular in Java but less idiomatic for Kotlin-specific features. |
MockK
MockK is a Kotlin-native mocking framework and the recommended choice for Kotlin projects due to its robust feature set.
Features
100% Kotlin-native: Designed with Kotlin’s features in mind (e.g., null safety, extension functions).
Coroutine support: Mocks suspend functions seamlessly.
Object/singleton mocking: Easily mocks Kotlin object declarations. Don't forget to unmock it after each test.
Extension function mocking: Supports mocking Kotlin-specific constructs.
Flexible verification: Verifies call order, counts, and more.
Static mocking: Replaces tools like PowerMock for mocking static methods.
Argument capturing: Captures and inspects arguments passed to mocks.
Relaxed mocks: Automatically returns default values for unmocked calls.
Annotations: Provides
@MockK
and@RelaxedMockK
for declarative mock setup.
Example
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
@Test
fun `should return greeting with user name from service`() {
val userService = mockk<UserService>()
every { userService.getUserName(1) } returns "Alice"
val userManager = UserManager(userService)
val result = userManager.getUserGreeting(1)
result shouldBe "Hello, Alice!"
verify(exactly = 1) { userService.getUserName(1) }
}
Mockito-Kotlin
Mockito-Kotlin is a wrapper around Mockito, a popular Java mocking library. While widely used, it’s not fully optimized for Kotlin due to its Java roots.
Example
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@Test
fun `should return greeting with user name from service`() {
val userService = mock<UserService>() {
on { getUserName(1) } doReturn "Alice"
}
val userManager = UserManager(userService)
val result = userManager.getUserGreeting(1)
result shouldBe "Hello, Alice!"
verify(userService, times(1)).getUserName(1)
}
Limitations
No native support for Kotlin-specific features: Mockito-Kotlin does not fully embrace Kotlin’s modern constructs, such as coroutines and extension functions, requiring workarounds or additional libraries (e.g., kotlinx-coroutines-test) to handle them effectively.
Limited documentation: The official Mockito-Kotlin documentation is sparse, offering minimal guidance compared to more Kotlin-native alternatives like MockK.
Best suited for Mockito familiarity: While it’s a viable option if you’re already comfortable with Mockito from Java projects, its limitations become apparent as your Kotlin usage grows. I recommend switching to MockK for a more seamless experience, especially as you encounter these constraints in practice.
Assertion Libraries
Assertions validate test outcomes. While JUnit 5 includes basic assertions (e.g., assertEquals), Kotlin developers often prefer richer, more expressive libraries.
Comparison Table
Framework | Popularity | Kotlin-Native | Features |
---|---|---|---|
Kotest Assertions | ⭐⭐⭐⭐⭐ | ✅ | Beautiful DSL, coroutine-friendly. |
AssertJ | ⭐⭐⭐⭐ | ❌ (Java-based) | Fluent API, powerful but less Kotlin-idiomatic (e.g., null safety issues). |
Strikt | ⭐⭐⭐ | ✅ | Clean API, lightweight, lazy assertions (inactive since mid-2024). |
Atrium | ⭐⭐ | ✅ | Kotlin-first, detailed messages, but complex and less popular. |
Kotest Assertions
Kotest’s assertion library (docs) is versatile and can be used with JUnit 5 or Kotest’s own framework.
Features
Infix style: Natural, readable syntax (e.g.,
shouldBe
,shouldContain
).Soft assertions: Collects all failures before reporting.
Rich matchers: Supports collections, strings, nullability, and more.
Examples
import io.kotest.matchers.shouldBe
import io.kotest.assertions.assertSoftly
@Test
fun `basic and soft assertions`() {
val numbers = listOf(1, 2, 3)
numbers.size shouldBe 3 // Basic assertion
assertSoftly {
numbers.contains(2) shouldBe true
numbers.last() shouldBe 3
} // Reports all failures together
}
AssertJ
AssertJ offers a fluent, strongly-typed API but is Java-based, leading to occasional friction with Kotlin’s type system (e.g., nullable types).
Example: Recursive Comparison
import org.assertj.core.api.Assertions.assertThat
data class Order(val id: Int, val items: List<Item>, val orderDate: String)
data class Item(val quantity: Int)
@Test
fun `compare orders ignoring fields`() {
val order1 = Order(1, listOf(Item(2)), "2023-01-01")
val order2 = Order(1, listOf(Item(2)), "2023-01-02")
assertThat(order1)
.usingRecursiveComparison()
.ignoringFields("orderDate")
.isEqualTo(order2)
}
Incompatibility
Nullable Type Incompatibility
import org.assertj.core.api.Assertions.assertThat
@Test
fun `nullable type handling with AssertJ`() {
val name: String? = null
assertThat(name).hasSize(4) // Issue: AssertJ ignores Kotlin nullability - better as compile error
}
Double Comparison Issue
import org.assertj.core.api.Assertions.assertThat
data class Item(val price: Double)
@Test
fun `compare items with custom precision`() {
val item1 = Item(12.34)
val item2 = Item(12.34000000000001)
assertThat(item1)
.usingRecursiveComparison()
.withComparatorForType(DoubleComparator(0.0005), Double::class.java)
.isEqualTo(item2) // Fail because use the default comparator for java.lang.Double instead.
}
Notes
Useful for complex object comparisons but requires extra configuration for Kotlin-specific needs.
AssertK is known as a simplified version of AssertJ for Kotlin, but it lacks powerful APIs such as complex object comparisons.
@Test
fun `nullable type handling with AssertK`() {
val name: String? = null
//assertThat(name).hasLength(4) compile error
assertThat(name).isNotNull().hasLength(4)
}
Strikt
Strikt is a Kotlin-native assertion library with a clean, chainable API (docs).
Features
Verbose diagnostics for debugging.
Soft assertions via
expect
blocks.Custom assertions are extension functions.
Example
import strikt.api.expectThat
import strikt.assertions.hasLength
import strikt.assertions.startsWith
@Test
fun `string assertions`() {
val subject = "The Enlightened take things Lightly"
expectThat(subject)
.hasLength(35)
.startsWith("L")
}
Output (if fails)
Expect that "The Enlightened take things Lightly":
✓ has length 35
✗ starts with "L"
found "T"
Notes
API is practical and adequate, but not as extensive or feature-rich as some other assertion libraries.
Development has stalled (inactive for ~7 months as of March 2025).
Atrium
Atrium is a Kotlin-first assertion library with detailed error messages and flexible APIs.
Features
Supports fluent and infix styles.
Soft assertions via
expect
blocks.Verbose diagnostics for debugging.
Example
import ch.tutteli.atrium.api.fluent.en_GB.toContain
import ch.tutteli.atrium.api.fluent.en_GB.atLeast
import ch.tutteli.atrium.api.fluent.en_GB.butAtMost
import ch.tutteli.atrium.api.fluent.en_GB.entries
import ch.tutteli.atrium.api.fluent.en_GB.toBeLessThan
import ch.tutteli.atrium.api.verbs.expect
@Test
fun `complex list assertion`() {
expect(listOf(1, 2, 2, 4))
.toContain.inAnyOrder
.atLeast(1).butAtMost(2)
.entries { toBeLessThan(3) }
}
Output (if fails)
I expected subject: [1, 2, 2, 4]
◆ to contain, in any order:
⚬ an element which needs:
» to be less than: 3
⚬ ▶ number of such elements: 3
◾ is at most: 2
Notes
Powerful but complex API may deter beginners.
Less popular due to competition from Kotest and others.
Conclusion
For Java developers moving to Kotlin:
Start with JUnit 5 for familiarity, then explore Kotest for a modern, Kotlin-native experience.
Use MockK for mocking due to its seamless Kotlin integration.
Choose Kotest Assertions for a balance of power and simplicity, or AssertJ for complex object comparisons if you’re comfortable with its Java roots. If a feature’s missing, you can always write custom verification logic to get the job done.
Experiment with these tools to find the best fit for your project!
How to Setup
This section guides you through setting up a Kotlin testing environment using JUnit 5 (Jupiter) as the test framework, MockK for mocking, and Kotest Assertions for expressive assertions. These tools form a robust, Kotlin-friendly testing stack, optimized for Java developers transitioning to Kotlin. We’ll use Gradle with Kotlin DSL for the build configuration.
Prerequisites
A Kotlin project (e.g., created with IntelliJ IDEA or Gradle).
Gradle as your build tool.
Kotlin version 2.0.0
Step-by-Step Setup with Gradle (Kotlin DSL)
1. Add Dependencies
Edit your build.gradle.kts
file to include the necessary dependencies:
plugins {
kotlin("jvm") version "2.0.0" // Adjust to your Kotlin version
}
group = "org.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
// JUnit 5 (Jupiter) for test framework
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
// MockK for mocking
testImplementation("io.mockk:mockk:1.13.17")
// Kotest Assertions for assertions
testImplementation("io.kotest:kotest-assertions-core:5.8.0")
}
// Configure test tasks
tasks.test {
useJUnitPlatform() // Required for JUnit 5
}
kotlin {
jvmToolchain(21) //JDK version
}
2. Verify Setup
Here’s a realistic example: testing a UserManager
class that depends on a UserService
to fetch user data. We’ll mock the service, test the manager’s logic, and verify the interaction.
Create these files in your project:
src/main/kotlin/com/example/UserManager.kt
:
class UserManager(private val userService: UserService) {
fun getUserGreeting(userId: Int): String {
val name = userService.getUserName(userId)
return "Hello, $name!"
}
}
interface UserService {
fun getUserName(userId: Int): String
}
src/test/kotlin/com/example/UserManagerTest.kt
(test code):
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Test
class UserManagerTest {
@Test
fun `should return greeting with user name from service`() {
val userService = mockk<UserService>()
every { userService.getUserName(1) } returns "Alice"
val userManager = UserManager(userService)
val result = userManager.getUserGreeting(1)
result shouldBe "Hello, Alice!"
verify(exactly = 1) { userService.getUserName(1) }
}
}
Run the test with:
./gradlew test
If successful, you’ll see a passing test report in the console or build output.
Explanation
Mocking: We mock
UserService
to simulate fetching a user’s name without a real implementation.Testing:
UserManager
uses the mock, and we test its logic (constructing a greeting).Assertion: Kotest’s
shouldBe
checks the output.Verification: MockK’s verify confirms the service was called as expected, ensuring correct dependency interaction.
Why This Stack?
JUnit 5: Familiar to Java developers, widely supported, and integrates seamlessly with Kotlin.
MockK: Kotlin-native, ideal for mocking coroutines, extension functions, and singletons.
Kotest Assertions: Enhances readability and flexibility without requiring a full switch to Kotest’s framework.
This setup provides a solid foundation for unit testing in Kotlin, balancing familiarity with modern Kotlin-specific features.
For more information, let's Like & Follow MFV sites for updatingblog, best practices, career stories of Forwardians at:
Facebook: https://www.facebook.com/moneyforward.vn
Linkedin: https://www.linkedin.com/company/money-forward-vietnam/
Youtube: https://www.youtube.com/channel/UCtIsKEVyMceskd0YjCcfvPg


Tạo lập & quản lý API Specs dành cho microservice của Go với Swagger
