skip to content

If you switch a nested Kotest spec from the default single-instance behaviour to IsolationMode.InstancePerLeaf, what re-executes for each test — the spec body, container blocks, beforeSpec, beforeTest — and what does that cost you?

level: seniorimportance: should knowfreq 30%

answer

  1. fresh instance ⇒ spec body re-runs and re-registers
  2. container path re-executes per leaf, not replayed
  3. beforeSpec is per instance → multiplied
  4. duplicated effects = correctness bug, not just slowness
  5. cheaper: InstancePerRoot, or refactor to immutable fixtures

basics

~20 s

Each leaf gets a new spec instance, so the spec body re-runs to re-register the tree, the containers on the path to that leaf re-execute, and beforeSpec fires once per instance. Per-test hooks still fire per test case. Cost: multiplied setup and duplicated container side effects.

solid answer

~50 s

With `IsolationMode.InstancePerLeaf` Kotest constructs the spec once per leaf test. For each leaf it: 1. constructs the class and runs the **spec body**, re-registering the whole test tree; 2. fires **beforeSpec** for that new instance — so a spec-level fixture runs once per leaf, not once per class; 3. executes the **container blocks on the path** down to the target leaf (their bodies really run again, they are not replayed from a cache), firing `beforeContainer`/`beforeTest` for them; 4. runs the leaf with the per-test callbacks around it. The costs are runtime and duplicated side effects. Anything a container body *does* — insert a row, start a service, bump a counter, register a global stub — now happens once per leaf beneath it, so "exactly one row" assertions and mock-invocation counts break. Anything expensive in `beforeSpec` (a container, a bound port) is multiplied and may collide. The usual response is to move heavy setup out of per-instance scope entirely, or to use the coarser `InstancePerRoot` mode.

code

kotlin · 12 lines
kotlin
class ReExecutionDemo : FunSpec({
    println("spec body")          // 1x by default, 3x under InstancePerLeaf

    context("outer") {
        println("outer container") // 1x by default, 3x under InstancePerLeaf
        test("one") { }
        test("two") { }
        test("three") { }
    }
}) {
    override fun isolationMode() = IsolationMode.InstancePerLeaf
}

go deeper

for a junior

Know that a fresh instance means the spec body runs again and the containers above the test re-execute.

for a middle

Give the per-leaf sequence and note that beforeSpec fires per instance.

for a senior

Lead with duplicated side effects as a correctness risk, the multiplied heavy setup, and how you would instrument to confirm.

for a principal

Weigh isolation against runtime for the whole suite and argue for declarative containers plus project-scoped heavy resources instead of blanket mode changes.

## What a "fresh instance" actually implies Kotest's spec body is not a static description that the engine parses once; it is executable Kotlin that registers tests as it runs. So constructing a new spec instance necessarily means **running that body again**. Under `InstancePerLeaf`, that happens once per leaf test. For a leaf buried two containers deep, the sequence per leaf is: 1. **Construct the spec** → the spec body executes, registering the whole tree again and re-creating every property declared in it. 2. **beforeSpec fires** for this instance — spec-level callbacks are per *instance*, not per class. 3. **Walk the path**: the outer container body executes, then the inner container body executes; each is a test case, so `beforeContainer`/`beforeTest`/`beforeAny` fire for them. 4. **Run the target leaf** with `beforeEach`/`beforeTest` before it and the `after…` counterparts after it. 5. **afterSpec fires** for that instance once its work is done. Siblings of the target leaf are registered but not executed in that instance — that is precisely how the isolation is achieved. ## Counting the re-executions ```kotlin class S : FunSpec({ println("body") context("outer") { println("outer") test("one") { } test("two") { } test("three") { } } }) ``` Under the default single-instance behaviour: `body` once, `outer` once, three tests. Under `InstancePerLeaf`: `body` three times, `outer` three times, three tests. The multiplier is the number of leaves under each container — which in real suites with five to twenty tests per context is a five- to twenty-fold increase in the cost of that container's setup. ## Where it hurts **Duplicated side effects.** This is the big one, and it is a *correctness* problem, not just a speed problem. Container bodies in the wild often do work: seeding a table, publishing an event, configuring a global stub, incrementing a counter that a later assertion checks. Re-executing the container per leaf repeats that work. Tests that asserted "one row exists" now see N. Verification of "the collaborator was called once" now sees N if the collaborator is a shared singleton mock. The fix is to make container bodies *declarative* — build values, do not perform effects — and put effects in lifecycle hooks whose firing count you control. **Multiplied spec-level setup.** A `beforeSpec` that starts an embedded server or a test container now runs per instance. Best case the suite crawls; worst case the second start fails because the port is bound or the container name is taken, and the spec errors out. Heavy resources belong at the project level (one per run) or behind a lazily-initialised singleton whose second call is a no-op — precisely because their lifecycle should not follow the spec's instance count. **Non-obvious interaction with expensive registration.** If the spec body itself does work — reading a fixture file, computing a large data set for `withData` — that too repeats per leaf. ## What does *not* change - Per-test callbacks still fire per test case, exactly as before; the mode does not add or remove them. - Global state is untouched: companion objects, singletons, static caches, database rows and files survive re-instantiation, so a leak living there is unaffected by the switch. - Assertions, matchers and test names are unchanged. ## Choosing a cheaper point on the curve `InstancePerTest` is strictly more re-execution than `InstancePerLeaf`, since containers get instances of their own as well. In the other direction, `InstancePerRoot` (added in Kotest 6.0 as the recommended mode, where the other two fresh-instance modes are deprecated) gives one instance per top-level test case: each root's containers execute once, and everything under that root shares the instance. If the pollution you are fighting is between top-level branches, that buys the isolation you need at a bounded cost. The cheapest option of all is usually not a mode change: keep the default, make spec-level and container-level declarations immutable, and rebuild the mutable parts in `beforeTest`/`beforeEach`. Fresh-instance modes are the blunt instrument for suites you cannot easily refactor. ## Diagnosing after a switch When someone flips the mode and the suite goes red or slow, the diagnostic is mechanical: add a `println` (or a counter) in the spec body and in each container body, run one spec, and count. The counts tell you immediately which block is being multiplied, and the fix is to move the effect out of that block into a hook or out of the spec entirely. ## What interviewers listen for The explicit statement that the spec body and the container path re-execute; `beforeSpec` being per instance; the correctness angle (duplicated effects) rather than only the speed angle; and knowing a cheaper mode exists.

  • After switching to InstancePerLeaf, a test that asserts a table contains exactly one seeded row starts failing with more rows than expected. Why?
    The seeding almost certainly lives in a container body. That body re-executes once per leaf beneath it, so the insert repeats and the row count grows. Move the effect into a per-test hook that also cleans up, or make the container purely declarative and let each test seed exactly what it needs.
  • Your beforeSpec starts an embedded server, and under InstancePerLeaf the spec fails with a port already in use. What is the right fix?
    `beforeSpec` is per spec instance, so a fresh-instance mode starts one server per leaf. Hoist the server to a resource shared across the run — project-level configuration or a lazily-started singleton whose second call is a no-op — and keep only per-test resetting (clearing stubs, wiping state) in the spec's hooks.
  • How would you confirm which block is being re-executed without reading the whole spec?
    Instrument and count: put a counter or a print statement in the spec body and at the top of each container body, then run the single spec and compare the counts to the number of leaves. The block whose count equals the leaf count is the one being multiplied, which points straight at the effect to relocate.

saying these in an interview costs you the question

  • Believing the spec body is parsed once and only the leaf re-runs
  • Assuming beforeSpec still runs exactly once per class after switching modes
  • Treating the change as purely a performance matter and missing duplicated side effects
  • Thinking InstancePerTest is cheaper than InstancePerLeaf
  • Expecting global or database state to be reset by the mode change

context