skip to content

Kotest's WordSpec builds tests out of `should` and `When` blocks. What does a WordSpec declaration look like, how are the reported names assembled from it, and how deeply can it nest?

level: juniorimportance: should knowfreq 28%

answer

  1. "subject" should { "behaviour" { } }
  2. Keyword is appended into the name — "a stack should"
  3. Optional outer When layer; capital W because `when` is a Kotlin keyword
  4. Fixed depth: When → should → leaf
  5. Uniform shape, but no room for a third dimension

basics

~20 s

You write "a stack" should { "pop returns the last item" { ... } }. The should block is a container whose name renders as "a stack should"; the inner strings are leaf tests. An optional outer When block adds one scenario layer. Nesting is fixed: When → should → test.

solid answer

~50 s

**WordSpec** composes test names out of English words supplied by the DSL. The core form is an infix `should` on a string: ```kotlin "a stack" should { "return the last item on pop" { } } ``` The `should` block is a **container**; Kotest renders its name with the keyword appended, so it reads *"a stack should"*, and the inner string-plus-lambda entries are **leaf tests**. Read top-to-bottom the path is a sentence. An optional outer layer expresses a scenario: `"a stack" When { "empty" should { "throw on pop" { } } }`. The function is capitalised as `When` because `when` is a Kotlin soft keyword and would otherwise need backticks. The crucial mechanical point is that **WordSpec is fixed-depth**: at most a `When` container, then a `should` container, then leaves. You cannot nest `should` inside `should` or add a third container level — the DSL scopes simply do not expose it. That rigidity is the style's selling point and its limit.

code

kotlin · 11 lines
kotlin
class StackTest : WordSpec({
    "a stack" When {
        "empty" should {
            "throw on pop" { shouldThrow<NoSuchElementException> { stack.pop() } }
            "report size zero" { stack.size shouldBe 0 }
        }
        "holding one item" should {
            "return that item on pop" { stack.pop() shouldBe 1 }
        }
    }
})

go deeper

for a junior

Recall the "subject" should { "behaviour" { } } shape, that the keyword becomes part of the reported name, and that there is an optional When layer.

for a middle

Add the fixed two-container depth and the compile-time reason you cannot nest further, plus the naming convention the assembled sentence forces.

for a senior

Discuss the trade: enforced uniformity across a suite versus no room for a third condition dimension, and what you do when a subject outgrows it.

for a principal

Frame it as a convention-over-configuration choice for a large suite — predictable report shape and onboarding cost against expressive ceiling — and where you would allow exceptions.

## The idea Most Kotest nested styles let you write any heading you like and nest as far as you want. **WordSpec** takes the opposite bet: it supplies the connecting words itself and caps the structure, so every spec in the codebase comes out with the same grammatical shape. ## The two-level form ```kotlin class StackTest : WordSpec({ "a stack" should { "return the last pushed item on pop" { stack.push(1); stack.push(2) stack.pop() shouldBe 2 } "report its size" { } } }) ``` Here `should` is an **infix function on String** taking a lambda. It registers a container. The container's displayed name is your string with the keyword appended — *"a stack should"* — and each inner `"..." { ... }` entry is a leaf, registered by the same string-invoke convention Kotest uses in its flat string style. The reported path therefore reads as one sentence: *a stack should → return the last pushed item on pop*. Because the framework supplies the verb, the naming convention is forced on you in a useful way: the outer string is a **noun phrase** (the subject under test) and the inner strings are **predicates** (the expected behaviour). Writing "should" inside either string produces a stuttering name. ## Adding the scenario layer When the same subject behaves differently in different states, you wrap the `should` blocks in a `When` container: ```kotlin class StackTest : WordSpec({ "a stack" When { "empty" should { "throw on pop" { } "report size zero" { } } "holding one item" should { "return that item on pop" { } } } }) ``` The capital `W` is not a style choice — `when` is a Kotlin soft keyword, so calling it unquoted would clash; Kotest exposes the capitalised `When` so you can write it without backticks. The rendered path becomes something like *a stack → when empty → should throw on pop*, which is why the strings are written to slot into that sentence. ## Fixed depth is the design This is the mechanical fact interviewers are looking for. WordSpec allows **at most two container levels** — an optional `When`, then a `should` — with leaves underneath. There is no way to nest a `should` inside a `should`, or to add a third scenario layer. The DSL scope inside a `should` block offers only leaf registration, so the attempt is a compile error, not a runtime surprise. The consequences cut both ways: - **In favour:** every WordSpec in the repo has the same shape. Reports are uniform, names are grammatical, and nobody invents a five-deep hierarchy that only its author can navigate. New readers know exactly where to look. - **Against:** a subject with genuinely multi-dimensional conditions ("when logged in, and on mobile, and with the flag on") has nowhere to put the third dimension. You either flatten it into a longer `When` string — *"logged in on mobile with the flag on"* — and accept combinatorial repetition, or you split the file, or you move that spec to a style with unbounded nesting. ## Suspending bodies and container bodies As everywhere in Kotest, the lambdas are **suspending**, so a leaf can call suspend functions directly. Note that a `should` or `When` body is not a setup hook: it executes as part of the run to register and run its children. Statements you drop at the top of a container body run at that point in the execution, which is a different thing from a lifecycle callback, and their re-execution depends on the spec's isolation mode. Put shared setup in Kotest's lifecycle callbacks rather than inline in a container. ## Which style is this, exactly WordSpec sits alongside Kotest's other container-bearing styles. What distinguishes it is that the *keywords* are part of the rendered name rather than mere syntax: in most styles your string is the whole name, whereas in WordSpec the framework contributes "should" (and "when"). Candidates who have only used styles where the string is the name are frequently surprised by the assembled output the first time they read a WordSpec report. ## What to say in an interview Name the shape (`"subject" should { "behaviour" { } }`), state that the keyword is appended into the reported name, mention the optional `When` layer and why it is capitalised, and — the part that separates a real user from someone who read the docs once — say that the nesting depth is **fixed at two containers**, and explain the trade that buys.

  • Why is Kotest's WordSpec scenario function written as `When` with a capital W?
    Because `when` is a Kotlin keyword used for the when-expression, so an unquoted lowercase call would clash with the language syntax. Kotest exposes the capitalised `When` so the DSL reads naturally without forcing backticks around the identifier at every call site.
  • A team wants three levels of grouping in a WordSpec. What are their options?
    They cannot add a third container level — WordSpec caps nesting at an optional `When` and a `should`, and the inner scopes do not expose further containers. The realistic choices are to fold the extra dimension into a longer `When` string, to split the subject across more spec files, or to move that particular spec to a Kotest style with unbounded nesting.

saying these in an interview costs you the question

  • Writing the word "should" inside the test strings, producing a stuttering reported name
  • Claiming WordSpec supports arbitrary nesting like other container styles
  • Assuming the reported name is exactly the string you passed, with no keyword appended
  • Thinking the capital `W` in `When` is a naming preference rather than avoidance of the Kotlin keyword
  • Treating a `should` block body as a setup hook

context