skip to content

Lifecycle & Configuration

Kotest replaces JUnit's annotation-driven lifecycle with DSL hooks, per-spec isolation modes, listeners/extensions, and a project-wide config class. Interviewers focus here because isolation and configuration mistakes are the root of most flaky or state-leaking Kotest suites.

on this pageshow

explore

questions

29

In a Kotest spec, how do you run setup code before every test and cleanup code after every test, and what do those callbacks receive?

level: juniorimportance: must knowfreq 45%

answer

  1. beforeTest { testCase } / afterTest { (testCase, result) }
  2. declared in the spec body, position irrelevant
  3. both are suspend — no runBlocking
  4. before-hook throws → test not run, reported failed
  5. per test case, not per spec

basics

~20 s

Declare Kotest's beforeTest { } and afterTest { } blocks in the spec body. beforeTest receives the TestCase about to run; afterTest receives a TestCase/TestResult pair. Both are suspend lambdas, so suspending setup and cleanup work directly.

solid answer

~50 s

Kotest registers lifecycle callbacks as DSL blocks inside the spec body: ```kotlin class MySpec : FunSpec({ beforeTest { testCase -> /* setup */ } afterTest { (testCase, result) -> /* cleanup */ } test("…") { } }) ``` `beforeTest` runs before each test case in that spec and is handed the `TestCase` (name, config, descriptor) so you can branch on it. `afterTest` takes a `Pair<TestCase, TestResult>` — hence the destructuring — so cleanup can see whether the test passed, failed or errored. Both are `suspend`, so you can call suspending setup without `runBlocking`. Position inside the body doesn't matter: the whole spec body runs first to register tests and callbacks, then the engine executes tests. If `beforeTest` throws, the test does not run and is reported as failed; an exception thrown from `afterTest` is reported too, so cleanup that can fail should be defensive.

code

kotlin · 17 lines
kotlin
class OrderServiceTest : FunSpec({
    lateinit var repo: InMemoryOrderRepository

    beforeTest { testCase ->
        repo = InMemoryOrderRepository()
    }

    afterTest { (testCase, result) ->
        repo.close()
        println("${testCase.name} -> $result")
    }

    test("saves an order") {
        repo.save(Order("A-1"))
        repo.count() shouldBe 1
    }
})

go deeper

for a junior

Recall the two block names, that they go in the spec body, and that afterTest can see the result.

for a middle

Add that the callbacks are suspend, that hooks fire per test case, and that nested specs make beforeTest fire for containers too.

for a senior

Talk about failure semantics, keeping teardown defensive, and choosing per-test versus per-spec scope for cost reasons.

for a principal

Frame it as fixture strategy: how much state a hook should own at all, and why tests with no shared mutable state need fewer hooks.

## What a lifecycle hook is in Kotest A Kotest spec is a Kotlin class whose body (the lambda passed to `FunSpec`, `DescribeSpec`, `WordSpec`, …) *registers* tests rather than running them. When the engine executes that spec it walks the registered tests and invokes callbacks around them. Those callbacks are the lifecycle hooks. Kotest exposes them as ordinary DSL functions you call inside the spec body, so no annotations are involved. ## beforeTest and afterTest The two everyday hooks are `beforeTest` and `afterTest`: ```kotlin class OrderServiceTest : FunSpec({ lateinit var repo: InMemoryOrderRepository beforeTest { testCase -> repo = InMemoryOrderRepository() } afterTest { (testCase, result) -> repo.close() } test("saves an order") { /* … */ } }) ``` `beforeTest` is invoked before each test case of the spec and receives the `TestCase` — the engine's model of the test, carrying its name, its per-test config and its descriptor. That argument is what lets you write conditional setup (`if (testCase.name…)`), though branching on names is usually a smell; prefer putting the test in its own spec or using tags. `afterTest` receives a `Pair<TestCase, TestResult>`, which is why idiomatic code destructures it as `afterTest { (testCase, result) -> … }`. The `TestResult` tells you the outcome, so you can dump diagnostics only for failures — capture a screenshot, print the container log, snapshot the database. Both lambdas are `suspend`. Kotest runs the whole test pipeline inside a coroutine, so suspending setup (starting an embedded server, awaiting a migration, calling a suspending API) is written straight, with no `runBlocking` wrapper. ## Where the blocks go and when they are registered Declare the blocks at the root of the spec body. Because the body executes once purely to register things, and tests only run afterwards, the *position* of a `beforeTest` block relative to the tests does not matter — a hook declared at the bottom of the body still applies to the first test. You can register more than one block of the same kind; all of them run. Do not rely on a subtle relative ordering between several registrations: if the order of two setup steps matters, put both steps in one block. ## Failure semantics If `beforeTest` throws, the test body is not executed and the test is reported as failed/errored — this is a feature: a broken fixture should fail loudly rather than run a test against half-built state. An exception thrown from `afterTest` is not swallowed either; it surfaces as a failure against the test, which is why teardown that legitimately may fail (closing an already-closed resource) should be written defensively. Because cleanup failures can mask the real assertion failure, keep `afterTest` small. ## What these hooks are *not* - They are **per test case**, not per spec. Expensive shared resources (a started container, a bound HTTP port) belong in `beforeSpec`/`afterSpec`, which run once per spec instance. - In a **nested** spec (contexts inside contexts), `beforeTest` fires for the container test cases as well as the leaf tests. If you only want leaves, Kotest has `beforeEach`; for containers only, `beforeContainer`. In a flat spec every test is a leaf, so the difference is invisible until someone adds nesting. - They do **not** by themselves reset object state that lives outside the spec instance. A `companion object` counter, a singleton, rows written to a real database — none of that is undone because a hook ran. Reset such state explicitly. ## Alternative registration form The same callbacks exist as overridable `suspend` member functions on the spec class (`override suspend fun beforeTest(testCase: TestCase)`), which is how teams share setup through an abstract base spec. The DSL block and the override both feed the same machinery, and both fire if both are present. ## What interviewers are checking That you know setup is a first-class DSL call rather than a JUnit-style annotation; that you know `afterTest` sees the result; that hooks are suspending; and that you know the difference between per-test and per-spec scope well enough not to start a database container before every single test.

  • Why does the afterTest lambda use destructuring parentheses?
    Because the callback is typed as taking a single `Pair<TestCase, TestResult>` argument rather than two parameters. Kotlin lets you destructure a Pair in a lambda parameter position, so `afterTest { (testCase, result) -> … }` is just component1/component2 on that pair. You can equally write `afterTest { it.first }`, but the destructured form is idiomatic.
  • Where would you put code that must run once for the whole spec, like starting an embedded server?
    In `beforeSpec`/`afterSpec`, which fire once per spec instance instead of once per test. Starting a server per test is usually far too slow. Be aware that under a fresh-instance isolation mode the spec is instantiated more than once, so `beforeSpec` runs per instance, not strictly once per class.
  • What happens if the cleanup in your afterTest block throws?
    Kotest reports the exception against the test rather than ignoring it, so a passing test can be turned red by broken teardown. Keep teardown minimal and defensive — guard against already-closed resources — so a cleanup failure does not hide the real assertion result.

saying these in an interview costs you the question

  • Thinking Kotest needs @BeforeEach-style annotations rather than DSL blocks
  • Assuming a beforeTest declared after the tests only applies to later tests
  • Believing beforeTest fires only for leaf tests in a nested spec
  • Putting expensive one-time setup such as starting a container in beforeTest
  • Assuming an exception in a before/after hook is swallowed and the test still runs normally

context

open as a page

In Kotest 5, what are the ways to register a listener or extension, and how does the registration site change what it affects?

level: middleimportance: must knowfreq 42%

basics

~20 s

Inside a spec via extension(...) or override fun extensions() — that spec only. In a class extending Kotest's AbstractProjectConfig via override fun extensions() — every spec in the run. Or @AutoScan for classpath-discovered global extensions.

open as a page

Kotest offers beforeTest, beforeEach, beforeContainer and beforeAny. In a nested spec where tests live inside a context block, which of these fires for which test cases?

level: middleimportance: must knowfreq 40%

basics

~10 s

Kotest treats containers and leaves both as test cases. beforeEach/afterEach fire only for leaf tests; beforeContainer/afterContainer only for containers; beforeTest/afterTest and their aliases beforeAny/afterAny fire for both.

open as a page

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.

level: middleimportance: must knowfreq 45%

basics

~20 s

SingleInstance: 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.

open as a page

How does Kotest 5 locate your AbstractProjectConfig class at runtime, and what would you check if its settings appear to be ignored?

level: middleimportance: must knowfreq 36%

basics

~20 s

Write one class (or object) extending AbstractProjectConfig with a no-arg constructor. Kotest 5 picks it up by convention from the io.kotest.provided package on the test classpath, or from the fully qualified name given in the kotest.framework.config.fqn system property. One config applies per run.

open as a page

Which test settings can you set globally in Kotest's AbstractProjectConfig, and what wins when a spec or an individual test declares the same setting?

level: middleimportance: must knowfreq 40%

basics

~10 s

Project config holds run-wide defaults: isolationMode, timeout and invocationTimeout, assertionMode, testCaseOrder, concurrency settings, duplicateTestNameMode, failOnEmptyTestSuite, and global extensions. Resolution is most-specific-wins: per-test .config() beats spec-level defaultTestConfig, which beats the project default.

open as a page

Explain Kotest's tagging system: how you attach a Kotest Tag to a spec or to an individual test, and how the kotest.tags system property expression decides which tests run — including what happens to tests that carry no tags at all.

level: middleimportance: must knowfreq 45%

basics

~20 s

You define a Tag (an object extending Kotest's Tag, or NamedTag("Slow")), attach it with the @Tags annotation on a spec or config(tags = setOf(...)) on a test, then filter with -Dkotest.tags using an expression like "Linux & !Slow". Pure exclusions keep untagged tests; adding an inclusion means only tests carrying that tag run.

open as a page

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.

level: seniorimportance: must knowfreq 40%

basics

~20 s

Kotest'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.

open as a page

You want a large Kotest suite to run faster by executing work in parallel. Explain the knobs Kotest gives you — concurrentSpecs, concurrentTests, parallelism, the @Isolate annotation and the blockingTest test-config flag — and what breaks when you turn them on.

level: seniorimportance: must knowfreq 35%

basics

~30 s

concurrentSpecs sets how many spec classes run concurrently and concurrentTests how many tests inside a spec do; both are coroutine concurrency. parallelism sets the number of threads the engine dispatches specs onto. @Isolate marks a spec that must never run alongside others, and config(blockingTest = true) gives a blocking test its own thread so it cannot starve the shared dispatcher. What breaks is anything global: shared databases, singletons, system properties, static mocks, fixed ports.

open as a page

Kotest lets you disable a test by using an x-prefixed builder (xtest, xcontext, xdescribe, xgiven), or by prefixing the test name with a bang (!), and lets you prefix a name with f: to focus it. Explain what each does, which tests they can be applied to, and which one wins when several are in play.

level: juniorimportance: should knowfreq 40%

basics

~20 s

x-builders and the ! bang prefix both register a test as disabled, so it is reported as skipped and a disabled container skips its whole subtree. f: focus runs only focused root tests in that spec. Bang beats focus, and both prefixes only work on root-level tests.

open as a page

What does Kotest's autoClose function do inside a spec, when does the resource get closed, and where does it let people down?

level: middleimportance: should knowfreq 28%

basics

~20 s

autoClose registers an AutoCloseable held by a spec so Kotest closes it during that spec's teardown, in reverse registration order. It is per spec instance, so under per-test isolation each instance opens and closes its own copy — it is not a shared or project-scoped resource mechanism.

open as a page

In Kotest you can register a lifecycle callback either as a DSL block in the spec body or by overriding a suspend function such as beforeTest on the spec class. How do the two differ, and what happens if an abstract base spec overrides beforeTest and a subclass also registers a beforeTest block?

level: middleimportance: should knowfreq 32%

basics

~20 s

Both feed the same callback machinery, so both run — the override does not replace the DSL block or vice versa. Overrides are suspend members inherited from the spec type, so they are the way to share setup through a base spec; DSL blocks are local to one spec body.

open as a page

How do you set Kotest's isolation mode for one spec versus for a whole project, and which setting wins when both are present?

level: middleimportance: should knowfreq 28%

basics

~20 s

Per spec: override the spec's isolationMode() function to return an IsolationMode. Project-wide: set the isolationMode property on your AbstractProjectConfig subclass. The spec-level setting takes precedence; specs that declare nothing fall back to the project default, and failing that, SingleInstance.

open as a page

Kotest's TestCaseOrder has the values Sequential, Random and Lexicographic. What does each do, what is the default, where do you configure it, and why would a team deliberately run tests in random order?

level: middleimportance: should knowfreq 35%

basics

~20 s

TestCaseOrder controls the order of sibling tests within a spec: Sequential is declaration order and is the default, Lexicographic sorts by name, Random shuffles. Set it per spec by overriding testCaseOrder(), or globally in the project config. Random is used to expose tests that secretly depend on each other's leftover state.

open as a page

In a Kotest 5 spec, what does defaultTestConfig do, and how does it differ from calling .config(...) on an individual test?

level: middleimportance: should knowfreq 30%

basics

~10 s

defaultTestConfig supplies a TestCaseConfig used as the baseline for every test in that spec, overriding project-wide defaults. Calling .config(...) on one test overrides that baseline again for that test only. Same settings, different scope.

open as a page

Kotest's test config accepts enabled, enabledIf and enabledOrReasonIf. What is the difference between the three, and why can enabled = someRuntimeCheck() behave differently from enabledIf = { someRuntimeCheck() }?

level: middleimportance: should knowfreq 32%

basics

~20 s

enabled is a plain Boolean fixed when the test is registered; enabledIf is a lambda taking the TestCase and evaluated later, when the engine decides whether to run that test; enabledOrReasonIf returns Kotest's Enabled value so the skip carries a printed reason. A value computed for enabled is frozen at registration time; the lambda is not.

open as a page

In Kotest 5, how do the beforeSpec/afterSpec listener callbacks differ from prepareSpec/finalizeSpec when a spec class produces more than one instance?

level: seniorimportance: should knowfreq 30%

basics

~20 s

beforeSpec/afterSpec fire once per spec instance, so under non-single isolation they fire repeatedly for the same class. prepareSpec/finalizeSpec fire exactly once per spec class — before the first instance is created and after the last one finishes, with finalizeSpec receiving all results.

open as a page

Kotest's TestCaseExtension exposes an intercept(testCase, execute) function. What is the contract, and what can it do that a plain afterTest listener callback cannot?

level: seniorimportance: should knowfreq 34%

basics

~20 s

intercept wraps the test: you receive the TestCase and an execute lambda, and must return a TestResult. Because you control whether execute runs, you can skip a test, surround it with setup/teardown that must bracket it, or replace the reported result — none of which a listener can do.

open as a page

When would you put setup in Kotest's beforeSpec/afterSpec rather than beforeTest/afterTest, and what should you know about when those spec-level callbacks actually fire?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Use beforeSpec/afterSpec for expensive resources shared by all tests in the class — a started server, a warmed cache. They fire once per spec instance, receive the Spec, and are skipped entirely when no test in the spec is active, so never treat beforeSpec as an unconditional class-loaded hook.

open as a page

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%

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.

open as a page

What does Kotest's assertionMode setting do when set to AssertionMode.Error in project config, and which test bugs does it catch?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Kotest counts assertions made through its own assertion library during a test. With AssertionMode.Error, a test that completes without executing any counted assertion fails; Warn logs instead; None disables the check. It catches tests that verify nothing.

open as a page

A Kotest run uses a tag filter that should exclude a whole spec, yet the spec's constructor side effects — starting a container, opening a connection — still happen. Explain why a class-level Kotest @Tags annotation and tags declared from inside the spec body differ here, and what other spec-level switches Kotest offers (@Ignored, @EnabledIf with an EnabledCondition).

level: seniorimportance: should knowfreq 28%

basics

~30 s

A tag declared by the class-level @Tags annotation can be read from the class without constructing the spec, so an excluded spec is never instantiated. Tags declared inside the spec body are only discoverable after construction, so the constructor and its side effects run before the filter can apply. Kotest also offers @Ignored to disable a spec class outright and @EnabledIf with an EnabledCondition for a programmatic spec-level gate.

open as a page

Hundreds of Kotest specs need one shared piece of infrastructure — say a single database. How would you use Kotest's listener and extension surface to own its lifecycle, and what tradeoffs do you weigh?

level: principalimportance: should knowfreq 24%

basics

~20 s

Give it exactly one owner: a ProjectListener (beforeProject/afterProject) registered once in project config, started lazily on first use, with per-test cleanup left to cheaper hooks. Per-spec hooks and autoClose multiply the cost by spec count and by isolation mode.

open as a page

Would you make a fresh-instance Kotest isolation mode the default for a whole test suite, or keep the single-instance default and manage state spec by spec? How do you decide?

level: principalimportance: should knowfreq 24%

basics

~20 s

Default to the cheap single-instance mode and treat shared mutable state as a defect to remove; opt individual legacy specs into a fresh-instance mode. Consider a suite-wide fresh-instance default only when you cannot refactor and correctness matters more than the multiplied runtime.

open as a page

You inherit a Kotest suite of several thousand tests that takes 40 minutes and runs entirely serially. How would you decide how much of it to run concurrently, and how would you keep the result trustworthy rather than intermittently red?

level: principalimportance: should knowfreq 25%

basics

~20 s

Measure first to find whether the time is IO-bound or dominated by a few slow specs, then raise Kotest's concurrentSpecs before concurrentTests, module by module. Run each step many times, @Isolate the specs that fail while tracking them as debt, fix shared state rather than serialising around it, and pair the rollout with randomised ordering so latent coupling surfaces deliberately.

open as a page

You own the test suite for a large multi-team codebase using Kotest. How do you decide which settings belong in the project-wide AbstractProjectConfig versus in individual specs?

level: principalimportance: should knowfreq 20%

basics

~20 s

Globalise invariants that should be uniform and fail loudly — empty-suite failure, duplicate-name policy, assertion mode, a safety-net timeout, cross-cutting extensions. Keep locally anything whose exception carries information, and anything that changes semantics silently, such as isolation mode.

open as a page

Kotest's test config accepts an invocations parameter. What does it do, which kinds of test can use it, and when is repeating a test body the wrong tool for the job?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

invocations runs a leaf test's body the given number of times; a failure in any repetition fails that one test case. It cannot be used on containers. It is the wrong tool for exploring input space — that is property testing — and a poor substitute for fixing a flaky test.

open as a page

Kotest's SpecExecutionOrder controls the order whole spec classes run in, with values including Undefined, Lexicographic, Random, Annotated and FailureFirst. Walk through what each means, how the @Order annotation participates, and what FailureFirst needs in order to work.

level: middleimportance: nice to knowfreq 25%

basics

~20 s

SpecExecutionOrder orders spec classes, not tests inside them. Undefined is the default and means discovery order; Lexicographic sorts by class name; Random shuffles; Annotated honours the @Order annotation, running annotated specs first in ascending order with unannotated ones after; FailureFirst runs previously failed specs first, which requires Kotest's persisted record of the last run's failures on disk.

open as a page

What is Kotest's ConstructorExtension for, and why can it only be registered at project level?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

ConstructorExtension lets you create spec instances yourself — instantiate(kclass) returns a Spec or null to fall through to the next extension or Kotest's default no-arg construction. It must be registered in project config because it runs before any spec instance exists.

open as a page