skip to content

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%

answer

  1. beforeSpec = once per spec INSTANCE, not per class
  2. expensive/shared → spec scope; dirty/per-test → test scope
  3. no active tests → spec callbacks skipped
  4. beforeSpec throws → tests don't run, spec fails
  5. afterSpec must be defensive; not crash-proof

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.

solid answer

~60 s

`beforeSpec { spec -> }` runs once per spec **instance**, before its first test; `afterSpec { spec -> }` runs after its last test. That makes them the home for setup too expensive to repeat per test — starting an embedded server, binding a port, building a heavyweight fixture — while `beforeTest` keeps per-test state clean (resetting stubs, truncating tables). Three things to know: 1. **Per instance, not per class.** Under the default single-instance mode that is once per class, but a fresh-instance isolation mode instantiates the spec repeatedly and `beforeSpec` fires for each instance — so "start a container in beforeSpec" can become N containers. 2. **Skipped when nothing runs.** If every test in the spec is filtered out by tags or disabled, Kotest does not run the spec's lifecycle callbacks, so they are not a reliable place for bookkeeping that must always happen. 3. **Failures are loud.** An exception in `beforeSpec` fails the spec rather than letting tests run against a half-built fixture; an `afterSpec` failure is reported too, so teardown must be defensive. For resources shared by *many* specs, hoist to the project-level configuration surface instead.

code

kotlin · 18 lines
kotlin
class ApiIntegrationTest : FunSpec({
    lateinit var server: EmbeddedServer

    beforeSpec {
        server = EmbeddedServer()
        server.start()
    }

    afterSpec {
        runCatching { server.stop() }   // defensive: never mask the real failure
    }

    beforeTest { server.resetStubs() }

    test("returns 200 for a known id") {
        server.get("/orders/1").status shouldBe 200
    }
})

go deeper

for a junior

Know the scope difference: beforeSpec once around the whole spec, beforeTest around each test.

for a middle

Add the cost argument and the canonical split — expensive object in beforeSpec, per-test reset in beforeTest.

for a senior

Emphasise per-instance semantics under non-default isolation, the skipped-when-inactive rule, and defensive teardown.

for a principal

Reason about where resources should live at all — spec, project, or an external harness — and about failure-domain blast radius when teardown does not run.

## The scope ladder Kotest gives you three useful scopes for fixture code, and choosing correctly is mostly a cost/isolation trade: - **Per test case** — `beforeTest`/`beforeEach`. Cheap, maximal isolation. Reset mocks, truncate tables, rebuild a small object graph. - **Per spec instance** — `beforeSpec`/`afterSpec`. Runs once around all the tests of that instance. Expensive, shared setup. - **Per project** — the project configuration surface, for a resource many specs share (one database, one broker). `beforeSpec` receives the `Spec` instance itself; `afterSpec` likewise. In practice you rarely use the argument except for logging or for reaching a property on a typed base spec. ```kotlin class ApiIntegrationTest : FunSpec({ lateinit var server: EmbeddedServer beforeSpec { server = EmbeddedServer().also { it.start() } } afterSpec { server.stop() } beforeTest { server.resetStubs() } test("returns 200 for a known id") { /* … */ } }) ``` That split is the canonical shape: the *expensive, reusable* thing is created once; the *dirty, per-test* thing is reset before every test. Getting it backwards — starting the server in `beforeTest` — is one of the most common causes of a slow Kotest suite. ## "Once per instance", not "once per class" This is the detail that separates a middle answer from a senior one. Kotest may construct the spec class more than once: that is exactly what the fresh-instance isolation modes do. `beforeSpec` fires once for **each instance the engine creates**, before that instance's first test. Under the default single-instance behaviour, one instance means one `beforeSpec` — which is why most people never notice. Turn on a per-leaf or per-test isolation mode and a `beforeSpec` that starts a container suddenly starts one per instance, and your suite time explodes (or your port is already bound and the whole spec errors). So the rule of thumb is: `beforeSpec` is *instance-scoped*. If you need something truly once per JVM run, own it at the project level or behind a lazily-initialised singleton whose second call is a no-op. ## Skipped specs skip their callbacks If every test in a spec is inactive — excluded by a tag expression, marked disabled, or filtered out by a test filter — Kotest has no work to do in that spec and its lifecycle callbacks do not fire. That is usually what you want (do not start a database for a spec nobody is running), but it means `beforeSpec` is not a "this class exists" hook. Registration, metric emission or global bookkeeping placed there will silently not happen on a filtered run, and people then chase a phantom bug in CI where a tag filter is active but locally everything works. ## Failure semantics A throwing `beforeSpec` is fatal for that spec: the tests are not run against a half-built fixture, and the failure is reported rather than swallowed. That is the correct behaviour — an unstarted server should not produce fifty confusing assertion failures. `afterSpec` failures are reported as well, which matters because teardown often runs against a resource that a failing test already broke. Write `afterSpec` so it cannot throw on top of the real failure: null-check, catch-and-log, or use a close helper that tolerates an already-closed resource. Kotest also has an auto-closing facility on the extension surface for exactly this reason. ## afterSpec versus a finally block Because `afterSpec` is a registered callback and not a `finally` inside your code, it runs even when tests fail — that is its point. But it does **not** protect you against a crashed JVM or a forcibly killed CI job; for containers and external resources, pair it with the tool's own reaper/lifecycle rather than assuming teardown always executes. ## Interaction with per-test hooks Ordering around a single test in a single-instance spec is: `beforeSpec` (once) → per-test callbacks around each test → `afterSpec` (once) after the last test. If you find yourself needing "before the first test only, but with per-test data", that is usually a design smell: keep the expensive object in `beforeSpec` and its mutable state reset in `beforeTest`. ## What interviewers listen for The cost argument for choosing spec scope; the precise statement "once per spec **instance**"; the awareness that filtered specs skip the callbacks; and defensive teardown. A candidate who says "beforeSpec runs once per class, always" has not operated a suite with a non-default isolation mode.

  • Your beforeSpec starts a test container and the suite becomes very slow after a colleague changes the spec's isolation mode. What happened?
    A fresh-instance isolation mode makes Kotest construct the spec class repeatedly, and `beforeSpec` fires once per instance — so a container is now started for every instance instead of once for the class. Either move the container to a project-level resource shared by all specs, guard the start behind a lazily-initialised singleton, or keep the spec on the single-instance default and reset per-test state in `beforeTest`.
  • Why is beforeSpec a bad place for global bookkeeping such as registering a metric or writing an audit line?
    Because Kotest skips a spec's lifecycle callbacks when the spec has no active tests — everything filtered out by a tag expression or disabled. On a filtered CI run your bookkeeping silently never happens, which is hard to debug because it works locally. Put unconditional work at the project level instead.

saying these in an interview costs you the question

  • Saying beforeSpec always runs exactly once per spec class regardless of configuration
  • Putting expensive resource startup in beforeTest for convenience
  • Assuming beforeSpec runs even when every test in the spec is filtered out
  • Writing afterSpec teardown that can throw and mask the real failure
  • Treating afterSpec as a guarantee that survives a killed JVM or CI job

context