skip to content

Kotest's ShouldSpec registers tests with `should("...") { }`. What does that call actually create, and how do you group several such tests inside one ShouldSpec?

level: juniorimportance: should knowfreq 30%

answer

  1. should(...) = leaf, context(...) = container
  2. name renders as "should …"
  3. should cannot nest inside should
  4. root-level should → flat; context → nested
  5. hybrid flat/nested style

basics

~20 s

should("...") { } registers one leaf test whose reported name reads as "should ...". Grouping is done with context("...") { } containers, which may hold should leaves and further nested contexts. should also works at the spec root.

solid answer

~50 s

In Kotest's **ShouldSpec**, `should("returns null for a blank input") { ... }` registers a single **leaf test** — a terminal test case with a body — and the framework composes its reported name from the `should` keyword plus your string, so it reads as *"should returns null for a blank input"* (hence you phrase the string as a continuation of the sentence). Grouping uses `context("...") { ... }`, a **container**: it holds `should` leaves and further nested `context` blocks, and contributes its name to the child's full path. `should` blocks are terminal — you cannot nest a `should` inside a `should`. ShouldSpec is therefore a *hybrid*: it behaves like a flat style when you write `should` at the spec root, and like a nested style when you introduce `context` layers. ```kotlin class SlugTest : ShouldSpec({ should("trim whitespace") { } // root-level leaf context("with unicode input") { should("strip accents") { } // nested leaf } }) ```

code

kotlin · 16 lines
kotlin
class SlugTest : ShouldSpec({
    should("trim surrounding whitespace") {
        slugify("  hi  ") shouldBe "hi"
    }

    context("with unicode input") {
        should("strip accents") {
            slugify("café") shouldBe "cafe"
        }
        context("and emoji") {
            should("drop non-letters") {
                slugify("a😀b") shouldBe "ab"
            }
        }
    }
})

go deeper

for a junior

Recall that should makes a single test, context groups tests, and a should cannot contain another should.

for a middle

Add the name derivation — Kotest prefixes "should" to your string, and container names concatenate into the leaf's full path — plus the fact that bodies are suspending.

for a senior

Frame ShouldSpec as a hybrid: flat at the root, nested once you add contexts, and explain the naming convention you would enforce so paths read as sentences.

for a principal

Discuss when a fixed leaf keyword helps (uniform, greppable names) versus when it constrains expression, and how that interacts with a codebase-wide style convention.

## What ShouldSpec is Kotest ships several *spec styles* — different DSLs that all produce the same underlying test tree. **ShouldSpec** is the style built around the keyword `should`. You extend it either by passing a lambda to the constructor (`class X : ShouldSpec({ ... })`) or by putting the DSL calls in an `init` block (`class X : ShouldSpec() { init { ... } }`). Both forms are equivalent; the lambda form is the more common idiom. ## Leaves and containers Kotest's test tree has exactly two kinds of node: - A **leaf** (terminal test) has a body of assertions and is what a report counts as a passing or failing test. - A **container** has no assertions of its own; its job is to declare child nodes and to contribute a name segment to their path. In ShouldSpec: - `should("...") { ... }` registers a **leaf**. You may not nest another `should` (or a `context`) inside it — the DSL scope inside a `should` body does not offer those functions, so the mistake is a compile error rather than a runtime surprise. - `context("...") { ... }` registers a **container**. Inside it you may write `should` leaves and further `context` blocks, nesting as deep as you find readable. Both `should` and `context` are legal at the spec root, which is what makes ShouldSpec unusual: a file that only uses root-level `should` calls is a flat list of tests, indistinguishable in shape from a StringSpec or a FunSpec, while a file that opens with `context` reads like a nested spec. ## Name derivation The string you pass is not the whole displayed name. Kotest prefixes the leaf with the keyword, so `should("return an empty list")` is reported as *"should return an empty list"*. The practical consequence is a naming convention: write the string as the **predicate of a sentence** whose subject is the surrounding context and whose verb is supplied by `should`. Writing `should("it should return an empty list")` produces a stuttering name in the report. When the leaf sits inside containers, the full test path is the concatenation of the container names and the leaf name, in declaration order — this is what appears in IDE test trees and in build reports, and it is what you match against when filtering by test name. ## Typical shape ```kotlin class PriceCalculatorTest : ShouldSpec({ context("an empty basket") { should("cost zero") { calculate(emptyList()) shouldBe 0 } } context("a basket with one item") { context("and no discount") { should("cost the item price") { } } context("and a percentage discount") { should("apply the discount") { } } } }) ``` The context strings carry the *state or scenario*; the should strings carry the *expected behaviour*. Read top to bottom, each leaf's path is a sentence. ## Where the body runs Every `should` and `context` body is a **suspending** lambda, so you can call suspend functions directly in a test without wrapping them in a blocking builder. Bodies of containers are not setup hooks: a container body executes as part of the run, in order, to register and run its children. If you need setup, prefer Kotest's lifecycle callbacks rather than statements dropped at the top of a `context`. ## Choosing it ShouldSpec appeals to teams who want behaviour-flavoured test names without the ceremony of a full given/when/then structure, and it scales down gracefully: small specs stay flat, and you introduce `context` only when a group genuinely needs a shared heading. The cost is that `should` is fixed as the leaf keyword, so it cannot express a test name that is not naturally phrased as "should ...". ## Common mistakes Expecting `should` to nest is the usual one — people try to write an outer `should("handle errors")` grouping inner `should` calls, which does not compile. The other is confusing this `should` (a **test registration function** taking a name and a lambda) with the matcher infix functions such as `shouldBe`, which assert inside a test body. They are unrelated APIs that happen to share a word.

  • Can you nest a `should` block inside another `should` block in Kotest's ShouldSpec?
    No. A `should` block is a leaf test and its body scope does not expose the registration functions, so the nesting attempt fails to compile. To group tests you must introduce a `context("...")` container, which can hold `should` leaves and further contexts.
  • How should you phrase the string passed to `should(...)` so the report reads well?
    Phrase it as the continuation of a sentence that already begins with the word "should", because Kotest prefixes the keyword to your string. Write `should("reject a blank name")`, not `should("it should reject a blank name")`, otherwise the reported name stutters. Any surrounding `context` names form the subject of that sentence.

saying these in an interview costs you the question

  • Thinking `should("...") { }` (test registration) is the same API as the `shouldBe` matcher
  • Trying to nest `should` inside `should` to build groups instead of using `context`
  • Assuming ShouldSpec cannot have root-level tests and always requires a container
  • Repeating the word "should" inside the test string, producing duplicated wording in the report

context