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?
answer
- context = container (nests freely), expect = terminal leaf
- Both legal at the spec root → hybrid flat/nested
- Keyword is injected: renders as "expect <string>"
- Phrase the string as the object, not a clause
- Context body is not a setup hook
basics
~20 scontext("...") 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 linesclass 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
Recall that context groups and expect is the actual test, and that an expect cannot contain more tests.
Add unbounded context nesting, root-level legality of both functions, and the keyword-injected name derivation with its phrasing convention.
Note that context bodies are executed registration code rather than hooks, and that fixtures placed there behave differently under different isolation modes.
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