Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
154: Behavior-Driven Testing with Kotest Specs
When I first started using Kotest, I treated BehaviorSpec like a standard JUnit test class. I figured I could just instantiate my objects at the top of the spec and call it a day. It seems intuitive, but in the world of BDD (Behavior-Driven Development) specs, this is a recipe for "flaky" tests that pass in isolation but fail when run as a suite. Let me show you what I mean.
class ShoppingCartSpec : BehaviorSpec({
val cart = ShoppingCart() // The " innocent" mistake
Given("a shopping cart") {
When("an item is added") {
cart.addItem("Kotlin in Action")
Then("the cart should contain 1 item") {
cart.size shouldBe 1
}
}
When("another item is added") {
cart.addItem("Clean Code")
Then("the cart should contain 1 item") {
// This fails! It actually contains 2 items.
cart.size shouldBe 1
}
}
}
})
If you run the first When block, it passes. But the second one fails miserably. Why? Because val cart was initialized once when the spec was created. The first test modified the cart object, and since that same instance is shared, the second test inherits the state from the first. This is a state leak, and it's one of the most frustrating bugs to debug in a large test suite.
The Shared State Trap
The problem here is a misunderstanding of how Kotest's BehaviorSpec handles its lifecycle. In a BDD spec, the Given, When, and Then blocks aren't just labels; they are nested scopes. However, variables declared in the outer scope of the BehaviorSpec constructor are effectively singletons for the duration of that spec's execution.
You might think, "Why not just call cart.clear() at the start of every block?" You could, but that's manual labor that leads to more bugs. If you forget to clear the state in just one test, you've reintroduced the leak. We want our tests to be isolated.
Isolating State within the Given Block
The fix is to move the instantiation inside the Given block. In Kotest, the Given block is designed to set up the context for the behaviors that follow. By moving the variable inside, we ensure that we are defining the state within the scope of the behavior.
class ShoppingCartSpec : BehaviorSpec({
Given("a shopping cart") {
// Now the cart is scoped to this Given block
val cart = ShoppingCart()
When("an item is added") {
cart.addItem("Kotlin in Action")
Then("the cart should contain 1 item") {
cart.size shouldBe 1
}
}
When("another item is added") {
// Wait—this still feels wrong, right?
// If we want a FRESH cart for every 'When',
// we need to rethink the nesting.
}
}
})
Actually, I just walked you into a second trap. Even inside the Given block, if you have multiple When blocks, they are still sharing that one cart instance because they are siblings under the same Given. To truly isolate every single scenario, you have two choices: create the object inside the When block, or change the IsolationMode.
I prefer using IsolationMode.InstancePerLeaf. This tells Kotest to create a brand new instance of the entire spec class for every single Then block. It's the "nuclear option" for isolation, and it makes your tests incredibly robust.
class ShoppingCartSpec : BehaviorSpec({
isolationMode = IsolationMode.InstancePerLeaf
val cart = ShoppingCart() // Now this is safe!
Given("a shopping cart") {
When("an item is added") {
cart.addItem("Kotlin in Action")
Then("the cart should contain 1 item") {
cart.size shouldBe 1
}
}
When("another item is added") {
cart.addItem("Clean Code")
Then("the cart should contain 1 item") {
cart.size shouldBe 1 // Now this passes!
}
}
}
})
By setting the isolationMode, Kotest effectively clones the environment for every single leaf node (the Then blocks). Now, you can write your BDD specs naturally without worrying about whether a previous test left a "mess" in your objects.
📋 Practical Task
Implementing a User Permission System with BehaviorSpec
You need to test a PermissionManager class. This class has a method grantPermission(user: String, permission: String) and hasPermission(user: String, permission: String): Boolean.
Build a BehaviorSpec test suite that validates the following behaviors:
- Given an empty permission manager, when a permission is granted to "Alice", then Alice should have that permission.
- Given an empty permission manager, when a permission is granted to "Alice", then "Bob" should NOT have that permission.
- Given a permission manager where Alice already has "READ" access, when she is granted "WRITE" access, she should now have both permissions.
Constraint: You must use IsolationMode.InstancePerLeaf to ensure that the permissions granted in one test case do not leak into the others, and your tests must follow the Given -> When -> Then structure strictly.
There are no comments for now.