A Kotest spec passes when you run a single test on its own but fails when the whole class runs, and the spec body holds a mutable list the tests add to. Explain the mechanism and the ways to fix it.
answer
- SingleInstance default → one object, shared properties
- container bodies run once too — state shared by leaves
- fix ladder: local state → reset in beforeTest → isolation mode
- reassign rather than clear
- companions, singletons, DB rows survive any mode
basics
~20 sKotest's default SingleInstance mode constructs the spec once, so a mutable property in the spec body is one object shared by every test; earlier tests leave data behind. Fix by resetting it in beforeTest, keeping state local to each test, or switching isolation mode.
solid answer
~60 sUnder the default `IsolationMode.SingleInstance` Kotest creates the spec class once and runs every test against that instance. A `val items = mutableListOf<…>()` in the spec body is therefore **one list for the whole class**, and the same applies to state built inside a `context` block, whose body also runs once. Run one test and the list is empty; run the class and it carries whatever earlier tests appended — a classic order-dependent failure that also explains "passes locally, fails in CI" when execution order or filtering differs. Fixes, in the order I'd try them: 1. **Make state local** — declare it inside the test body. Best fix: nothing to leak. 2. **Reset it in `beforeTest`** — `beforeTest { items.clear() }`, or `lateinit var` reassigned per test. 3. **Change isolation** — `override fun isolationMode() = IsolationMode.InstancePerLeaf` (or `InstancePerRoot`) so each test gets a fresh instance; costs re-execution of the spec body and container path. And know the limit: no isolation mode resets `companion object` fields, singletons, or rows in a real database.
code
kotlin · 23 lines// Leaky: one list for the whole class under the default SingleInstance mode
class CartTestLeaky : FunSpec({
val cart = mutableListOf<String>()
test("one item") { cart += "apple"; cart shouldHaveSize 1 }
test("another") { cart += "pear"; cart shouldHaveSize 1 } // fails: 2
})
// Fix A: rebuild per test
class CartTestFixed : FunSpec({
lateinit var cart: MutableList<String>
beforeTest { cart = mutableListOf() }
test("one item") { cart += "apple"; cart shouldHaveSize 1 }
test("another") { cart += "pear"; cart shouldHaveSize 1 }
})
// Fix B: fresh spec instance per leaf test
class CartTestIsolated : FunSpec({
val cart = mutableListOf<String>()
test("one item") { cart += "apple"; cart shouldHaveSize 1 }
test("another") { cart += "pear"; cart shouldHaveSize 1 }
}) {
override fun isolationMode() = IsolationMode.InstancePerLeaf
}go deeper
Say that the default mode reuses one spec instance so the list is shared, and that building it inside each test fixes it.
Add the beforeTest rebuild and the isolation-mode option, and explain container bodies sharing state too.
Diagnose it as order dependence, rank the fixes by cost, and state clearly what isolation mode cannot isolate.
Turn it into policy: immutable spec-level declarations, randomised ordering in CI to expose order dependence, and no retry-on-flake culture.
## The mechanism A Kotest spec body is a lambda that runs once *per spec instance* to register tests. Anything you declare in it — `val queue = mutableListOf<String>()`, `var counter = 0`, a mock, a builder — is a property of that instance, created when the body runs. The default isolation mode is `SingleInstance`: Kotest constructs the class once and executes every test in it against that one object. So a mutable property is shared by all tests in declaration order. Test 1 appends two items; test 2 asserts `list shouldHaveSize 1` and sees three. In isolation test 2 passes; in the suite it does not. The same reasoning applies one level down in nested specs: a `context("…") { }` body also runs once under SingleInstance, so anything built inside a container is shared by all leaves beneath it — a subtler version of the same bug, because the state *looks* scoped to the group. ```kotlin class CartTest : FunSpec({ val cart = mutableListOf<String>() // one list for the whole class test("adding one item") { cart += "apple" cart shouldHaveSize 1 // passes } test("adding another item") { cart += "pear" cart shouldHaveSize 1 // fails: size is 2 } }) ``` ## Why it shows up as flakiness The failure is order-dependent, so anything that changes order or membership changes the outcome: running a single test from the IDE, a tag filter that removes the polluting test, a different spec ordering setting, or someone inserting a new test in the middle. Teams often mislabel this as flakiness and retry it away, which hides a genuine defect in the test design and sometimes in the code under test (a shared cache that should not be shared). ## The fixes, ranked ### 1. Don't share state at all Move the declaration into the test body. Each test then constructs exactly what it needs and nothing survives. For a small object graph this is the cheapest, clearest fix and it makes the test readable in isolation — a property good tests have anyway. ### 2. Rebuild or reset per test in a hook When construction is non-trivial, keep the declaration at spec level but rebuild it: ```kotlin lateinit var cart: MutableList<String> beforeTest { cart = mutableListOf() } ``` Reassigning is safer than mutating (`cart.clear()`), because reassignment cannot miss a field that a nested structure accumulated. Use `beforeEach` rather than `beforeTest` in a nested spec if you want the reset only for leaf tests. ### 3. Change the isolation mode ```kotlin override fun isolationMode() = IsolationMode.InstancePerLeaf ``` Now Kotest constructs a fresh spec instance per leaf test, re-running the spec body (and the container path down to that leaf), so every test sees a newly built list. It is the most sweeping fix and the most expensive: the body and containers re-execute per leaf, side effects inside container bodies are repeated, and the suite gets slower. Use it when a spec genuinely needs per-test freshness for many pieces of state, not as a reflex for one leaky list. On Kotest 6 `InstancePerRoot` is a cheaper middle ground when the pollution is between top-level branches rather than between siblings. ## The limit worth stating unprompted Isolation mode isolates the **spec object**. It does nothing about: - `companion object` / top-level / `object` state — per class or per process, not per instance; - singletons and static caches inside the code under test; - external systems: database rows, files, message queues, an in-memory server that keeps running; - global mock or stub registries. If a test still leaks after switching modes, the state lives in one of those places, and the answer is an explicit reset in a lifecycle hook, a fresh external resource per spec, or removing the global state from the design. ## Diagnosing it quickly Run the failing test alone — if it passes, you have order dependence. Then bisect: run the suspect pair together. Reading the spec for `var`/mutable collections declared outside test bodies usually finds the culprit in seconds. A useful discipline in review: spec-level declarations should be immutable and stateless (a service under test with no mutable fields, a pure fixture), and everything mutable should be created per test. ## What interviewers listen for Naming SingleInstance and explaining *why* one instance implies shared state; giving more than one fix and ranking them by cost; and the maturity signal — knowing that switching isolation mode does not clean up companions, singletons or the database.
- Why prefer reassigning a lateinit fixture in beforeTest over calling clear() on it?Reassignment guarantees a fully fresh object graph, while `clear()` only resets the one field you remembered — nested collections, cached derived values or listeners registered on the old object survive. Rebuilding is also cheaper to reason about in review: the reader does not have to verify that every mutable member was reset.
- You switch the spec to a fresh-instance isolation mode and the tests still interfere. What do you look for next?State that is not part of the spec instance: companion objects and top-level properties, singletons or static caches in the production code, and external systems such as the database, files or a running server. Isolation mode only re-constructs the spec class, so those must be reset explicitly in a hook, or the shared resource must be replaced per spec.
- How would you stop this class of bug from recurring across the codebase?Make it a review rule that spec-level declarations are immutable and per-test mutable state is created inside the test or rebuilt in a hook. Run the suite in a randomised or reversed order periodically so order dependence surfaces early, and treat a test that only passes in one order as a defect rather than a flake to retry.
saying these in an interview costs you the question
- Blaming flakiness or the CI machine instead of shared spec state
- Believing each Kotest test gets a fresh spec instance by default
- Assuming state built inside a container block is automatically scoped per leaf
- Reaching straight for the finest isolation mode instead of removing the shared mutable state
- Thinking a fresh-instance mode also resets companion objects or the database