Kotest's `withData` generates test names from the input values. What mechanisms does Kotest give you to override that generated name, and when would you pick each one?
answer
- type side: WithDataTestName, @IsStableType
- call site: nameFn overload, Map<String,T> keys
- @IsStableType grants permission, doesn't supply a name
- map keys are used verbatim — duplicate keys collapse in the map
- third-party or binary payload → nameFn
basics
~20 sFour hooks: implement WithDataTestName on the type, annotate the class @IsStableType so its toString is trusted, pass the nameFn overload withData({ "case ${it.id}" }, values), or pass a Map<String, T> whose keys become the names verbatim.
solid answer
~50 sKotest offers two type-side hooks and two call-site hooks. **Type side:** implement `io.kotest.datatest.WithDataTestName` and return a name from `dataTestName()` — best when the type is yours and every spec wants the same label. Or annotate the class `@IsStableType`, which tells Kotest its hand-written `toString()` is deterministic and safe to use as a name. **Call site:** the `nameFn` overload, `withData(nameFn = { "discount for ${it.tier}" }, cases) { ... }`, and the map overload, `withData(mapOf("empty cart" to c1, "single item" to c2)) { ... }`, where the keys are the test names exactly as written. Pick the call-site overloads for third-party types you cannot modify, for payloads whose `toString()` is huge or binary, and when the same type deserves different labels in different specs. Pick the type-side hooks when one canonical name should follow the value everywhere.
code
kotlin · 17 linesclass NamingTest : FunSpec({
context("nameFn — trims a noisy toString to what identifies the case") {
withData(
nameFn = { "status ${it.status} retry=${it.retry}" },
listOf(HttpCase(500, true), HttpCase(400, false)),
) { case -> shouldRetry(case.status) shouldBe case.retry }
}
context("map — keys become the test names verbatim") {
withData(
mapOf(
"empty cart is free" to Cart(),
"over threshold is free" to Cart(item(60_00)),
),
) { cart -> shipping(cart) shouldBe 0 }
}
})go deeper
Name at least the nameFn overload and know that data classes give readable names by default.
List all four mechanisms and state which are type-side versus call-site, plus a reason to choose each.
Argue from operations: names are identifiers used by re-runs, filters and CI reporting, so favour short deterministic labels and reserve the map overload for curated scenario lists.
Set the codebase convention — inputs are data classes or implement WithDataTestName, binary/large payloads always use nameFn — and pair it with a duplicate-name policy so bad names surface as build failures.
## The default and why you override it `withData` (package `io.kotest.datatest`, artifact `kotest-framework-datatest`) registers one test per element and derives each name from the value: `WithDataTestName` first, then an `@IsStableType`-annotated class's `toString()`, then a data class's generated `toString()`, otherwise a type-derived fallback. The default is fine for small data classes. It stops being fine when: - the type is not yours (a third-party request/response object, a JDK type wrapper) and has no useful `toString()`; - the data class carries a large or binary property — a byte array, a full JSON body, a 40-field aggregate — so the generated name is a paragraph or contains `[B@1f2a3b`; - the name you want is a *description of the case* ("expired token", "cart over free-shipping threshold"), which no `toString()` will ever produce; - the same input type is reused in several specs where different aspects matter. ## Mechanism 1 — `WithDataTestName` ```kotlin class Scenario(val id: String, val body: ByteArray) : WithDataTestName { override fun dataTestName(): String = "scenario $id" } ``` The interface has a single method, `dataTestName(): String`, and it wins over every other rule. Use it when the type is a first-class test fixture in your codebase and there is one obviously right label. The advantage is that every call site gets good names for free and nobody has to remember to pass a lambda. The cost is that the naming decision is baked into the type. ## Mechanism 2 — `@IsStableType` ```kotlin @IsStableType class Money(val cents: Long) { override fun toString(): String = "$${cents / 100}.${cents % 100}" } ``` This does not supply a name; it grants **permission**. It tells Kotest "this class's `toString()` is hand-written and deterministic — trust it". Without the annotation, a non-data class falls back to the type-based name even if you did override `toString()`. Use it when the class already prints itself well and you do not want a second, parallel naming method. ## Mechanism 3 — the `nameFn` overload ```kotlin withData( nameFn = { "HTTP ${it.status} -> retry=${it.shouldRetry}" }, responses, ) { case -> ... } ``` The first parameter is a `(T) -> String`. It is available on the vararg, collection and sequence forms. This is the workhorse for third-party types and for trimming a noisy `toString()` down to the one or two fields that identify the case. It also keeps the naming local, so two specs can label the same type differently. ## Mechanism 4 — the map overload ```kotlin withData( mapOf( "empty cart" to Cart(), "single item under threshold" to Cart(item(5_00)), "two items over threshold" to Cart(item(60_00), item(10_00)), ), ) { cart -> shipping(cart) shouldBe expected(cart) } ``` The map keys become the test names **verbatim** — no derivation, no truncation. This is the most readable option when the cases are a hand-curated list of named scenarios rather than a mechanically generated set, because the name is a specification sentence, not a dump of the input. The trade-off: the map is ordered by whatever `Map` implementation you pass, so use `mapOf`/`LinkedHashMap` if you care about report order, and remember duplicate keys silently collapse in a map literal — two cases with the same key means you lose one *before* Kotest ever sees them. ## Choosing between them A workable rule: - Input is a small data class you own and its `toString()` reads well → do nothing. - Input is yours, has one canonical short label, used in many specs → `WithDataTestName`. - Input is yours, already prints well, not a data class → `@IsStableType`. - Input is third-party, or the name should describe intent rather than content → `nameFn`. - Cases are a curated list of named scenarios → the map overload. ## What good names buy you Generated names are identifiers, not decoration. They are what the IDE re-runs, what name-based test filters match, what a CI report shows a reviewer, and what a flaky-test dashboard groups on. Names must therefore be **stable** (same input, same name, every run), **unique** within their scope (or Kotest's duplicate-name handling starts appending indexes), and **short enough to read** in a report column. All four mechanisms exist to let you satisfy those three properties whatever the input type looks like.
- What is the difference between annotating a class `@IsStableType` and implementing `WithDataTestName`?`WithDataTestName` supplies the name — Kotest calls `dataTestName()` and uses the result. `@IsStableType` supplies only permission: it asserts that the class's own `toString()` is deterministic, so Kotest may use it instead of falling back to a type-derived name. Use the interface when the label differs from `toString()`, the annotation when `toString()` is already the label you want.
- Why might the map overload silently lose a test case?Because you build a `Map<String, T>` before Kotest sees it, and a map literal with two identical keys keeps only the last value. Two scenarios described with the same sentence collapse into one entry, so one case never gets registered and the report shows nothing missing. With the `nameFn` overload the collision instead reaches Kotest, which applies `DuplicateTestNameMode` and at least warns.
saying these in an interview costs you the question
- "Overriding toString() on a plain class is enough for Kotest to use it" — it is not; the class needs @IsStableType (or the value needs WithDataTestName).
- "@IsStableType provides the test name" — it only marks the class's toString() as trustworthy.
- "nameFn only exists on the vararg form" — it is available on the collection and sequence forms too.
- "The map overload's keys are just documentation" — they are the actual test names, and duplicate keys drop cases before Kotest ever sees them.
- "Any name works as long as it is unique" — names must also be stable across runs, or IDE re-runs, filters and CI history break.