Kotlin Test Assertions
Introduction
Test assertions are a fundamental part of automated testing. They're how we express our expectations about the behavior of our code. When writing tests in Kotlin, you have access to a variety of assertion libraries and styles that can make your tests more readable, powerful, and expressive.
In this guide, we'll explore how to write effective test assertions in Kotlin using popular libraries. Whether you're new to testing or looking to improve your assertion skills, this tutorial will help you write more reliable and maintainable tests.
Understanding Assertions
At their core, assertions verify that certain conditions are met during test execution. If an assertion fails, the test fails, indicating that something isn't working as expected.
A basic test with an assertion follows this pattern:
@Test
fun `two plus two should equal four`() {
val result = 2 + 2
assertEquals(4, result)
}
In this simple example, assertEquals
is the assertion. It checks that the actual value (result
) equals the expected value (4
).
Basic Assertions with JUnit
JUnit is one of the most commonly used testing frameworks for Kotlin. It provides a set of built-in assertions that cover most of your basic needs.
Common JUnit Assertions
Let's look at some commonly used JUnit assertions:
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
class BasicAssertionsTest {
@Test
fun `basic assertions examples`() {
// Equality
assertEquals(4, 2 + 2)
// Boolean assertions
assertTrue(10 > 5)
assertFalse(5 > 10)
// Null checks
val nullValue: String? = null
val nonNullValue = "Hello"
assertNull(nullValue)
assertNotNull(nonNullValue)
// Same object reference
val list1 = listOf(1, 2, 3)
val list2 = list1
assertSame(list1, list2)
// Not the same object reference
val list3 = listOf(1, 2, 3) // Different list instance with same values
assertNotSame(list1, list3)
}
}
Asserting Exceptions
Testing that code throws expected exceptions is also important:
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.lang.IllegalArgumentException
class ExceptionAssertionsTest {
@Test
fun `should throw exception when dividing by zero`() {
val exception = assertThrows<ArithmeticException> {
val result = 5 / 0
}
assertEquals("/ by zero", exception.message)
}
@Test
fun `should throw when age is negative`() {
fun validateAge(age: Int) {
require(age >= 0) { "Age cannot be negative" }
}
val exception = assertThrows<IllegalArgumentException> {
validateAge(-1)
}
assertEquals("Age cannot be negative", exception.message)
}
}
Advanced Assertions with KotlinTest/Kotest
Kotest (formerly known as KotlinTest) offers a more Kotlin-friendly approach to assertions with a fluent API, making your tests more readable and expressive.
Kotest Assertion Styles
Kotest provides multiple assertion styles to match your preferences:
Style 1: Infix notation
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.collections.shouldContain
import org.junit.jupiter.api.Test
class KotestInfixTest {
@Test
fun `infix assertion style examples`() {
// Value equality
"hello" shouldBe "hello"
// Negative assertions
"hello" shouldNotBe "world"
// String assertions
"Hello, world!" shouldContain "world"
// Collection assertions
listOf(1, 2, 3) shouldContain 2
}
}
Style 2: Verify DSL
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldStartWith
import org.junit.jupiter.api.Test
class KotestVerifyTest {
@Test
fun `using assertSoftly for multiple assertions`() {
data class User(val id: Int, val name: String, val email: String)
val user = User(1, "John Doe", "[email protected]")
// All assertions are evaluated even if some fail
assertSoftly(user) {
it.id shouldBe 1
it.name shouldBe "John Doe"
it.email shouldStartWith "john.doe"
}
}
}
Smart Assertions with Strikt
Strikt is a modern, powerful assertion library for Kotlin with a focus on providing helpful error messages.
import org.junit.jupiter.api.Test
import strikt.api.expectThat
import strikt.assertions.*
class StriktAssertionsTest {
@Test
fun `basic strikt assertions`() {
val name = "Kotlin"
expectThat(name)
.isA<String>()
.hasLength(6)
.startsWith("Ko")
.endsWith("lin")
}
@Test
fun `collection assertions`() {
val numbers = listOf(1, 2, 3, 4, 5)
expectThat(numbers)
.hasSize(5)
.contains(3)
.doesNotContain(10)
.containsExactly(1, 2, 3, 4, 5)
}
@Test
fun `composite object assertions`() {
data class Product(val id: Int, val name: String, val price: Double)
val product = Product(101, "Laptop", 999.99)
expectThat(product) {
get { id }.isEqualTo(101)
get { name }.isEqualTo("Laptop")
get { price }.isGreaterThan(500.0)
}
}
}
Real-World Testing Examples
Let's see how assertions are used in more practical scenarios:
Testing a User Service
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.Assertions.*
// The code we're testing
class UserService {
private val users = mutableMapOf<Int, User>()
data class User(val id: Int, val name: String, val email: String)
fun addUser(user: User) {
if (users.containsKey(user.id)) {
throw IllegalArgumentException("User with ID ${user.id} already exists")
}
users[user.id] = user
}
fun getUser(id: Int): User {
return users[id] ?: throw NoSuchElementException("User with ID $id not found")
}
fun getAllUsers(): List<User> = users.values.toList()
}
class UserServiceTest {
@Test
fun `should add and retrieve user successfully`() {
// Arrange
val userService = UserService()
val user = UserService.User(1, "John Doe", "[email protected]")
// Act
userService.addUser(user)
val retrievedUser = userService.getUser(1)
// Assert
assertEquals(user, retrievedUser)
}
@Test
fun `should throw exception when adding duplicate user`() {
// Arrange
val userService = UserService()
val user = UserService.User(1, "John Doe", "[email protected]")
// Act & Assert
userService.addUser(user)
val exception = assertThrows<IllegalArgumentException> {
userService.addUser(user)
}
assertTrue(exception.message!!.contains("already exists"))
}
@Test
fun `should throw exception when user is not found`() {
// Arrange
val userService = UserService()
// Act & Assert
val exception = assertThrows<NoSuchElementException> {
userService.getUser(999)
}
assertTrue(exception.message!!.contains("not found"))
}
@Test
fun `should return all users`() {
// Arrange
val userService = UserService()
val user1 = UserService.User(1, "John Doe", "[email protected]")
val user2 = UserService.User(2, "Jane Smith", "[email protected]")
// Act
userService.addUser(user1)
userService.addUser(user2)
val allUsers = userService.getAllUsers()
// Assert
assertEquals(2, allUsers.size)
assertTrue(allUsers.contains(user1))
assertTrue(allUsers.contains(user2))
}
}
Testing Data Processing Functions
import org.junit.jupiter.api.Test
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContainExactly
class DataProcessingTest {
// Function to test
fun processTemperatures(readings: List<Double>): Map<String, Double> {
if (readings.isEmpty()) return emptyMap()
return mapOf(
"average" to readings.average(),
"min" to readings.minOrNull()!!,
"max" to readings.maxOrNull()!!
)
}
@Test
fun `should calculate correct temperature statistics`() {
// Arrange
val temperatures = listOf(18.0, 23.5, 19.8, 25.2, 20.0)
// Act
val result = processTemperatures(temperatures)
// Assert
result["average"] shouldBe 21.3
result["min"] shouldBe 18.0
result["max"] shouldBe 25.2
}
// Function to test
fun filterAndTransform(input: List<String>): List<String> {
return input
.filter { it.length > 3 }
.map { it.uppercase() }
}
@Test
fun `should filter short strings and uppercase the rest`() {
// Arrange
val input = listOf("a", "ab", "abc", "abcd", "abcde")
// Act
val result = filterAndTransform(input)
// Assert
result shouldContainExactly listOf("ABCD", "ABCDE")
}
}
Best Practices for Test Assertions
-
Be specific: Make your assertions as specific as possible to catch unexpected behaviors.
-
One assertion per test: Generally, each test should verify a single behavior.
-
Use soft assertions when applicable for more efficient testing of multiple properties.
-
Write clear failure messages: Good assertions clearly explain what was expected and what actually happened.
-
Use the right assertion for the job: Different types of data or conditions may require different assertion styles.
-
Test both positive and negative cases: Don't just test that things work; test that they fail appropriately.
-
Be careful with floating-point assertions: Use delta values when comparing floating-point numbers.
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class FloatingPointTest {
@Test
fun `comparing floating point numbers`() {
val result = 0.1 + 0.2
// This might fail due to floating point precision
// assertEquals(0.3, result)
// Better approach with delta
assertEquals(0.3, result, 0.000001)
}
}
Summary
Test assertions are a crucial part of writing effective unit tests in Kotlin. In this guide, we've covered:
- Basic assertions with JUnit
- Advanced assertions with Kotest
- Smart assertions with Strikt
- Real-world testing examples
- Best practices for writing clear and effective assertions
By mastering these assertion techniques, you'll be able to write more expressive and reliable tests for your Kotlin applications. Remember that good tests not only verify that your code works correctly but also serve as documentation for how your code is intended to be used.
Additional Resources
Exercises
-
Write a test for a function that calculates the factorial of a number. Include assertions for valid inputs and error cases.
-
Create a data class representing a shopping cart item. Write tests for adding items to a cart, removing items, and calculating the total price.
-
Write tests using different assertion libraries (JUnit, Kotest, and Strikt) for the same functionality. Compare the readability and expressiveness of each approach.
-
Practice writing tests for edge cases, such as empty collections, null values, and boundary conditions.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)