skip to content

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