Kotest's IsolationMode controls how many instances of a spec class are created for a run. Walk through what SingleInstance, InstancePerRoot, InstancePerTest and InstancePerLeaf each do for a spec with nested containers.
answer
- mode = how many times the spec class is constructed
- Single 1 · PerRoot per top-level · PerLeaf per leaf · PerTest every node
- container path re-executes for each new instance
- cost = re-run body + containers, duplicated side effects
- InstancePerRoot added in Kotest 6.0, its recommended isolation
basics
~20 sSingleInstance: one instance for all tests. InstancePerRoot: a fresh instance per top-level test, everything nested under it shares that instance. InstancePerTest: a fresh instance per test case, containers included. InstancePerLeaf: a fresh instance per leaf test, with its container path re-executed.
solid answer
~50 sKotest's `IsolationMode` decides how often the spec class is instantiated, and therefore how much spec-level state is shared. - **SingleInstance** (the default): one instance; every test runs against the same object, so properties declared in the spec body are shared across all tests. - **InstancePerRoot**: a new instance per **root-level** test case. Everything nested inside that root runs in the one instance, in order. Coarse-grained isolation with a bounded instance count. - **InstancePerTest**: a new instance for **every** test case, containers included. For each one Kotest re-executes the path of containers leading to it, then runs just that node. - **InstancePerLeaf**: a new instance per **leaf** test. The container path down to that leaf is re-executed in the fresh instance; containers themselves do not get their own instance. The finer the mode, the more times the spec body and container blocks re-execute — that is both the isolation you bought and the cost you pay in runtime and duplicated side effects.
code
kotlin · 13 linesclass S : FunSpec({
// SingleInstance : 1 instance
// InstancePerRoot: 2 instances ("a", "b")
// InstancePerLeaf: 3 instances ("a1", "a2", "b")
// InstancePerTest: an instance per test case, containers included
context("a") {
test("a1") { }
test("a2") { }
}
test("b") { }
}) {
override fun isolationMode() = IsolationMode.InstancePerLeaf
}go deeper
Name the modes and give the one-line meaning of each; know that one of them is the default.
Count instances for a concrete nested spec and explain that container paths re-execute for each new instance.
Lead with the cost/duplication consequences and the fact that only spec-instance state is isolated.
Frame the choice as a suite-wide policy question — instance count versus runtime, and what belongs in shared external harnesses instead.
## What isolation mode actually controls A Kotest spec is a class whose body registers tests. `IsolationMode` answers one question: **how many times does Kotest construct that class during the run?** Everything else — what state is shared, which blocks re-execute, how expensive the suite is — falls out of the answer. ## The modes ### SingleInstance One instance for the whole spec. The body runs once, registering the tree; then all tests execute against that single object. Any `var`, `lateinit var` or mutable collection declared in the spec body is therefore **shared by every test**, and mutations made by one test are visible to the next. This is the default and the fastest mode; it is also the mode in which order-dependent tests hide. In a nested spec, a container body also runs once, so state built inside a `context { }` is shared by all leaves under it. ### InstancePerRoot A fresh instance per **root-level** test case — each top-level `test`/`context`/`describe`/`given`. Everything nested beneath one root executes inside that one instance, sequentially, so siblings deep in a tree still share state with each other, but two different top-level branches never do. The instance count is bounded by the number of roots, which keeps the cost predictable while removing the worst cross-branch leakage. ### InstancePerTest A fresh instance for **every** test case — containers as well as leaves. To run a given node, Kotest creates the instance, re-executes the spec body (which re-registers the tree), walks down the containers on the path to the target node executing them, and then runs the target. Because containers are themselves targets, a container's body ends up executing for the container's own "turn" as well as for each descendant's turn. Maximum isolation, maximum re-execution. ### InstancePerLeaf A fresh instance for each **leaf** test. The container path leading to that leaf is re-executed in the new instance so the leaf sees the same context, but containers do not get instances of their own. This is the mode people usually mean when they say "I want each test to start clean" in a nested spec: sibling leaves cannot see each other's mutations of spec-level or container-level state. ## A concrete count ```kotlin class S : FunSpec({ context("a") { test("a1") { } test("a2") { } } test("b") { } }) ``` Test cases: container `a`, leaves `a1`, `a2`, `b`. - **SingleInstance** — 1 instance; `a`'s body runs once. - **InstancePerRoot** — 2 instances (roots are `a` and `b`); `a1` and `a2` share one instance, so they still share whatever `a`'s body built. - **InstancePerLeaf** — 3 instances, one per leaf; `a`'s body re-executes on the way to `a1` and again on the way to `a2`. - **InstancePerTest** — an instance for each of the four test cases, with the container path re-executed each time. ## Why the trade exists More instances means more isolation and less spooky action at a distance, but every extra instance re-runs the spec body and the container path. If a container starts a service, seeds ten thousand rows or sleeps, that cost is multiplied. Worse, side effects in container bodies are *repeated*, not just recomputed: an insert placed in a container will happen once per leaf under a per-leaf mode, and "exactly one row" assertions start failing. Fresh instances also do nothing for state that does not live in the instance — `companion object` fields, singletons, static caches, real database rows and files all survive re-instantiation. ## Versions Assume Kotest 5.x. `SingleInstance`, `InstancePerTest` and `InstancePerLeaf` are long-standing; `InstancePerRoot` was added in Kotest 6.0 as a simpler, cheaper middle ground, and in Kotest 6 it is the recommended mode while `InstancePerTest` and `InstancePerLeaf` are deprecated. If you are writing new code on a recent version, reach for `InstancePerRoot` before the finer-grained modes. ## What interviewers listen for Counting instances correctly for a nested example; knowing that the container path re-executes rather than being replayed from a cache; naming the default; and — the mature point — knowing that isolation mode isolates the *spec object*, not the world.
- Under InstancePerLeaf, does a container block run once or once per leaf beneath it?Once per leaf beneath it. Each leaf gets a fresh spec instance, and Kotest re-executes the containers on the path down to that leaf in the new instance so the leaf sees the intended context. That is exactly why side effects written inside container bodies — inserts, counters, service starts — are duplicated under this mode.
- Two tests in the same spec still interfere even after you switch to a fresh-instance mode. What kinds of state would explain that?State that does not live in the spec instance: `companion object` or top-level properties, singletons and object declarations, static caches inside the code under test, and anything external such as database rows, files, message queues or a shared mock registered globally. Re-instantiating the spec cannot undo any of it — those need explicit reset in a lifecycle hook or a genuinely fresh external resource.
saying these in an interview costs you the question
- Saying InstancePerLeaf gives each leaf its own instance without the container path being re-executed
- Believing a fresh-instance mode also resets companion objects, singletons or the database
- Confusing InstancePerRoot with InstancePerLeaf in a deeply nested spec
- Assuming the finest mode is free and should be the project default
- Claiming the default is a per-test mode