skip to content

Flat Styles

The flat styles register tests as a simple list without containers — the fastest way to write and read small suites. You should know what each flat base class looks like and why AnnotationSpec exists mainly as a JUnit migration bridge.

on this pageshow

explore

questions

4

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

open as a page

In Kotest's StringSpec you write a test as a string literal immediately followed by a lambda — `"returns the length" { ... }`. What Kotlin mechanism makes that compile, and what structural limits does it place on a StringSpec file?

level: middleimportance: should knowfreq 35%

basics

~20 s

Inside the StringSpec body, Kotest brings an operator fun String.invoke(...) extension into scope, so "name" { } is really "name".invoke { }. Every registration is a root-level leaf: StringSpec has no containers, so no grouping or nesting is possible.

open as a page

How do you attach per-test configuration — a timeout, repeated invocations, or disabling a single case — to one individual test in Kotest's flat spec styles, and how does the syntax differ between StringSpec and FunSpec?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Both use a .config(...) builder, but it hangs off different things: in StringSpec it chains off the name string — "name".config(timeout = 2.seconds) { } — and in FunSpec off the registration call — test("name").config(invocations = 5) { }. AnnotationSpec has no equivalent.

open as a page

Kotest's AnnotationSpec lets you write tests as annotated methods instead of a DSL lambda. How does Kotest discover and name those tests, which lifecycle annotations does it provide, and what do you give up compared with Kotest's lambda-based flat styles?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

You extend AnnotationSpec() and mark methods with Kotest's own @Test; Kotest reflects over the class and each annotated method becomes a root-level test named after the method. It provides @BeforeEach/@AfterEach/@BeforeAll/@AfterAll, @Ignore, and @Test(expected = ...). You lose string test names, containers and the .config DSL.

open as a page