skip to content

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%

answer

  1. autoClose(x) returns x, closes at spec teardown
  2. reverse registration order
  3. autoClose(lazy { }) — build only if used
  4. per spec INSTANCE, not per class
  5. expensive resource → prepare/finalizeSpec instead

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.

solid answer

~50 s

```kotlin class DbTests : FunSpec() { private val db = autoClose(EmbeddedDb()) init { test("queries") { db.query("...") shouldBe ... } } } ``` `autoClose` takes an `AutoCloseable`, returns it (so you assign it straight to a property), and registers it for closing at spec teardown. Multiple resources close in reverse registration order, so a resource can safely depend on one registered earlier. There is also an overload taking a `Lazy<T>`, which only closes the value if it was actually initialised. The two things that bite: 1. **It is per spec instance.** Non-single isolation modes create several instances of the class, and each one constructs and closes its own resource. `autoClose` is not a once-per-class or once-per-project mechanism. 2. **Ordering against your own teardown.** If your `afterSpec` logic needs the resource still open, do not rely on the interleaving — close explicitly in `afterSpec` instead, or do the work before teardown.

code

kotlin · 8 lines
kotlin
class DbTests : FunSpec() {
    private val db = autoClose(EmbeddedDb())
    private val slow = autoClose(lazy { HeavyClient() }) // built only if touched

    init {
        test("queries") { db.rowCount() shouldBe 0 }
    }
}

go deeper

for a junior

Know the idiom: val db = autoClose(...) means Kotest closes it when the spec finishes.

for a middle

Add reverse close order, the Lazy overload, and the AutoCloseable-only limitation.

for a senior

Lead with the isolation-mode interaction and know which listener to move an expensive resource to.

for a principal

Treat it as a policy question — cheap instance-scoped resources use autoClose, expensive shared ones get an explicit class- or project-scoped lifecycle owner with external reaping for crash cases.

## What it is `autoClose` is a small convenience on `Spec`: ```kotlin fun <T : AutoCloseable> autoClose(closeable: T): T fun <T : AutoCloseable> autoClose(closeable: Lazy<T>): T ``` You wrap a resource at the point you create it, and Kotest remembers to close it when the spec is done. It returns the same object, so the idiom reads naturally: ```kotlin class DbTests : FunSpec() { private val db = autoClose(EmbeddedDb()) private val client = autoClose(HttpClient()) init { test("round trips") { /* uses db and client */ } } } ``` It replaces the boilerplate of an `afterSpec` that closes three things in the right order and remembers to null-check each one. ## When it fires At spec teardown — after the spec instance's tests have finished — and in **reverse order of registration**. Reverse order matters: if `client` was registered after `db` and depends on it, `client` closes first, mirroring how nested `use` blocks unwind. It fires whether the tests passed or failed. What it cannot promise is anything the JVM cannot promise: kill the process and no close runs. Resources whose leak survives the process (external containers, cloud fixtures) need external reaping regardless. Do not build logic that depends on a precise interleaving with your own `afterSpec` code. If a teardown step must observe the resource open, either do that work before the spec ends or drop `autoClose` and close explicitly in `afterSpec` where you control the order. ## The lazy overload ```kotlin private val db by lazy { EmbeddedDb() } // wrong: never registered private val db = autoClose(lazy { EmbeddedDb() }) // registered, closed only if used ``` The `Lazy` overload exists precisely so that an expensive resource is created only if some test touches it, while still being closed if it was. Wrapping the eager value defeats laziness (you construct it immediately); wrapping a plain `by lazy` delegate registers nothing at all. ## The isolation trap This is the point interviewers push on. `autoClose` is a method on a spec **instance** and registers into that instance. Under a non-single isolation mode Kotest creates a new instance per test (or per leaf), and each instance: 1. runs the property initialiser, constructing a new `EmbeddedDb()`, 2. registers it, 3. closes it at that instance's teardown. So a twenty-test spec opens and closes twenty databases. If the resource is cheap, this is a feature — perfect isolation. If it is a container or a server, it is a disaster, and nothing warns you: the diff that changed the isolation mode is nowhere near the spec that got slow. For a resource that should exist once per spec class, use `PrepareSpecListener`/`FinalizeSpecListener`; for once per run, a `ProjectListener` registered in project config. `autoClose` is the right tool only for per-instance resources. ## Other sharp edges - **It only knows `AutoCloseable`.** A resource with `stop()`, `shutdown()`, or `dispose()` needs a tiny adapter, or an explicit `afterSpec`. - **It closes; it does not reset.** Between tests within one instance, nothing happens. Per-test cleanup is a different hook. - **Registration happens when the call executes.** A resource created inside a test body and wrapped there is registered at that moment — usually you want it wrapped at spec construction so the intent is visible at the top of the file. - **Failures during close.** If `close()` throws, that surfaces during teardown rather than as a test failure; make close idempotent and tolerant so one broken resource does not obscure the real test result. ## Rule of thumb Use `autoClose` for cheap, instance-scoped `AutoCloseable`s where you want the close to be impossible to forget. The moment the resource is expensive or shared, move its lifecycle up to a class- or project-level listener and stop thinking of it as spec state.

  • Your spec uses autoClose for a Testcontainers-backed database and someone switches the spec to a per-test isolation mode. What breaks?
    Nothing fails, but every test now constructs and closes its own container, because the property initialiser and the autoClose registration both belong to the spec instance and a fresh instance is created per test. The suite becomes minutes-long. The fix is to move the container lifecycle to prepareSpec/finalizeSpec (once per class) or a ProjectListener (once per run) and leave only cheap per-test cleanup in the spec.
  • Why is there an autoClose overload that takes Lazy<T>?
    So an expensive resource is only constructed if a test actually uses it, while still being closed if it was constructed. Passing an already-built value would defeat the laziness, and registering a plain `by lazy` delegate registers nothing, so Kotest provides the overload to cover the combination explicitly.

saying these in an interview costs you the question

  • Treating autoClose as a project-wide or once-per-class resource mechanism
  • Assuming resources close in registration order rather than reverse
  • Wrapping a resource with a stop()/shutdown() method and expecting autoClose to call it
  • Believing autoClose runs even if the JVM is killed
  • Expecting autoClose to reset state between tests inside one spec instance

context