skip to content

Kotest's ExpectSpec uses `context("...") { }` and `expect("...") { }`. What does each of those register, where may each appear, and how do the reported names come out?

level: middleimportance: nice to knowfreq 20%

answer

  1. context = container (nests freely), expect = terminal leaf
  2. Both legal at the spec root → hybrid flat/nested
  3. Keyword is injected: renders as "expect <string>"
  4. Phrase the string as the object, not a clause
  5. Context body is not a setup hook

basics

~20 s

context("...") registers a container that may hold expects and further contexts, nesting freely; expect("...") registers a terminal leaf test. Both are legal at the spec root. The leaf reads as "expect <your string>", so the string is phrased as the expectation.

solid answer

~50 s

**ExpectSpec** is Kotest's container style built around the word *expect*. - **`context("...") { ... }`** registers a **container**: no assertions of its own, contributes a name segment, and its body scope re-exposes both functions so contexts nest to any depth. - **`expect("...") { ... }`** registers a **leaf** test. Its body scope offers no registration functions, so an `expect` is terminal and nesting inside it is a compile error. Both are legal **at the spec root**, so a spec can be a flat list of `expect` calls or a nested tree — the same hybrid shape Kotest's other keyword-based styles have. Like Kotest's should-flavoured style, ExpectSpec **contributes its keyword to the rendered name**: `expect("a 404 for an unknown id")` reads as *expect a 404 for an unknown id*. That drives the convention — write the string as a noun phrase completing the sentence, with the surrounding `context` names supplying the scenario, so the full path reads as prose.

code

kotlin · 13 lines
kotlin
class OrderApiTest : ExpectSpec({
    expect("a healthy service on startup") { health() shouldBe UP }

    context("GET /orders/{id}") {
        context("when the order exists") {
            expect("a 200 with the order body") { }
            expect("a cache header") { }
        }
        context("when the id is unknown") {
            expect("a 404") { }
        }
    }
})

go deeper

for a junior

Recall that context groups and expect is the actual test, and that an expect cannot contain more tests.

for a middle

Add unbounded context nesting, root-level legality of both functions, and the keyword-injected name derivation with its phrasing convention.

for a senior

Note that context bodies are executed registration code rather than hooks, and that fixtures placed there behave differently under different isolation modes.

for a principal

Position it against the other container styles — injected vocabulary and unbounded depth versus fixed-depth or free-form alternatives — as a house-convention decision.

## What ExpectSpec is for Kotest offers several container-bearing styles that differ mainly in the vocabulary they impose. ExpectSpec's vocabulary is **context / expect**: contexts describe the situation, expects describe what should be observed in it. It suits teams that want a light behavioural flavour without the full given/when/then ceremony, and who prefer *expect* to *should* as their assertion verb. ## The two functions **`context(name) { ... }`** creates a **container**. A container has no assertions of its own; its purpose is to declare children and add a segment to their path. Its body scope exposes both `context` and `expect` again, so you may nest contexts as deeply as you like and mix leaves and containers at the same level. **`expect(name) { ... }`** creates a **leaf** — the terminal test that carries assertions and that a report counts as a pass or failure. The scope inside an `expect` body is the *execution* scope, not a registration scope, so it exposes neither function. Attempting to nest inside an `expect` fails at compile time rather than surprising you at run time. Both functions are available at the spec root: ```kotlin class OrderApiTest : ExpectSpec({ expect("a healthy service on startup") { } // flat leaf at the root context("GET /orders/{id}") { context("when the order exists") { expect("a 200 with the order body") { } expect("a cache header") { } } context("when the id is unknown") { expect("a 404") { } } } }) ``` ## Name derivation The string you pass is not the whole displayed name for a leaf: Kotest composes it with the keyword, so an `expect("a 404")` renders as *expect a 404*. Containers render with their own string. The full path of a leaf is the chain of container names plus the composed leaf name, which is what IDE test trees and build reports show. The practical rule this creates: **write the `expect` string as the object of the sentence**, not as a full clause. `expect("a 404 for an unknown id")` reads correctly; `expect("it should return a 404")` renders as *expect it should return a 404*, which stutters. Container strings, by contrast, are free-form and typically carry the *scenario* — "when the order exists", "with an expired token". ## Suspending bodies and container-body semantics All bodies are **suspending**, so a leaf may call suspend functions directly without a blocking wrapper. A context body is **not a setup hook**. It executes as part of the run in order to register and run its children, so statements placed at the top of a context run at that point in the execution — and whether they re-run for each descendant leaf depends on the spec's isolation mode. Shared setup belongs in Kotest's lifecycle callbacks; putting a mutable fixture directly in a context body is the classic way to get results that change when the isolation mode does. ## Where it sits among the styles Compared with Kotest's other container styles, ExpectSpec is distinguished by three things worth naming in an interview: 1. It **injects its keyword** into leaf names, unlike the styles where your string is the entire name. 2. It has a **single container keyword** (`context`) with **unbounded depth**, unlike the fixed-depth styles that cap structure at two container levels. 3. It is a **hybrid**: root-level `expect` makes it flat, an opening `context` makes it nested, so a file can start simple and grow structure only where it is warranted. The cost of the injected keyword is expressive: any test name that does not read naturally as *"expect …"* fights the style, which is precisely the choice you are making when you adopt it. ## Common mistakes The two you actually see are repeating the verb inside the string (producing *expect it should expect…*-shaped names), and trying to nest an `expect` inside another `expect` to build a group, which does not compile — grouping is what `context` is for. A third, subtler one is assuming ExpectSpec requires a root container; it does not, and a small spec is perfectly idiomatic as a flat list of `expect` calls.

  • Can an `expect` block in Kotest's ExpectSpec contain another `expect`?
    No. An `expect` registers a terminal leaf, and the scope inside its body is the execution scope, which does not expose `context` or `expect`. The attempt fails to compile. Grouping is done with `context`, which may nest to any depth and may hold both leaves and further contexts.
  • How should the string passed to `expect(...)` be phrased?
    As the object of the sentence the keyword begins, because Kotest composes the reported name from the keyword plus your string. `expect("a 404 for an unknown id")` reads correctly, while `expect("it should return a 404")` renders as a stuttering name. Scenario wording belongs in the enclosing `context` strings instead.

saying these in an interview costs you the question

  • Believing `expect` blocks can nest to create groups
  • Repeating "expect" or "should" inside the leaf string, producing a stuttering reported name
  • Assuming ExpectSpec requires every test to sit inside a root container
  • Treating a `context` body as a before-hook that runs once for the whole subtree
  • Thinking the reported leaf name is exactly the string passed, with no keyword added

context