skip to content

Two elements passed to Kotest's `withData` produce the same generated test name. What does Kotest do, how do you change that behaviour, and why does it matter operationally?

level: seniorimportance: nice to knowfreq 18%

answer

  1. DuplicateTestNameMode: Warn (default) / Silent / Error
  2. AbstractProjectConfig.duplicateTestNameMode
  3. index suffixes are positional — shift when the list changes
  4. unique per scope, not globally
  5. root cause is the name derivation, not the mode

basics

~20 s

Kotest requires unique names within a scope, so its DuplicateTestNameMode decides: the 5.x default Warn logs a warning and appends an index to disambiguate. Set duplicateTestNameMode = DuplicateTestNameMode.Error in AbstractProjectConfig to fail instead. Duplicates break re-runs, filtering and CI history.

solid answer

~50 s

Test names are identifiers inside their scope, so a collision has to be resolved somehow. Kotest's `DuplicateTestNameMode` governs it: `Warn` (the Kotest 5.x default) logs a warning and disambiguates by appending an index; `Silent` disambiguates without complaining; `Error` fails, turning the collision into a build failure. You set it globally on `AbstractProjectConfig.duplicateTestNameMode`. Collisions in `withData` almost always mean the naming machinery fell back — the inputs are not data classes, don't implement `WithDataTestName` and aren't annotated `@IsStableType`, so every element got the same type-derived name. A `nameFn` that ignores a distinguishing field does the same. Operationally it matters because names are what the IDE re-runs, what name-based filters match, and what CI history and flaky-test dashboards key on. Index-suffixed names also shift when you reorder or insert a case, so yesterday's `Foo (3)` is today's `Foo (4)`. On a large data-driven suite, `Error` is the setting that keeps the report honest.

code

kotlin · 6 lines
kotlin
import io.kotest.core.config.AbstractProjectConfig
import io.kotest.core.names.DuplicateTestNameMode

object ProjectConfig : AbstractProjectConfig() {
   override val duplicateTestNameMode = DuplicateTestNameMode.Error
}

go deeper

for a junior

Know that Kotest test names must be unique in their scope and that duplicates usually mean the input type isn't naming itself well.

for a middle

Name DuplicateTestNameMode, its three values and the Warn default, and identify the fallback naming as the usual root cause.

for a senior

Argue the operational case: index suffixes are positional, so re-runs, name-based filtering and CI history break; set Error and fix the derivation upstream.

for a principal

Make it policy — Error in project config plus a convention that data-test inputs name themselves — so reporting quality is enforced by the build rather than by review.

## Why duplicates arise at all `withData` (package `io.kotest.datatest`) registers one node per element and derives each name from the value. If the type does not implement `WithDataTestName`, is not annotated `@IsStableType`, and is not a data class, Kotest falls back to a name based on the value's **type** rather than its contents — so all N elements ask for the same name. The other common source is a `nameFn` that projects away the field that actually distinguishes the cases (`{ "case for ${it.userId}" }` when three cases share a user id), and the map overload cannot collide at Kotest level at all because duplicate keys already collapsed inside the map literal before Kotest saw them. ## What Kotest does A test name has to be unique within its scope, because it is the address of that node. Kotest 5 exposes the policy as `DuplicateTestNameMode`: - **`Warn`** — the default. Logs a warning and disambiguates the name, typically by appending an index. The run stays green. - **`Silent`** — disambiguates with no warning. Only sensible when you have a deliberate reason to tolerate collisions. - **`Error`** — the collision fails, so the problem surfaces as a red build rather than an unreadable report. You set it project-wide in your `AbstractProjectConfig`: ```kotlin object ProjectConfig : AbstractProjectConfig() { override val duplicateTestNameMode = DuplicateTestNameMode.Error } ``` ## Why silent disambiguation is worse than it looks The default keeps the build green, which is exactly why the defect survives review. Three concrete costs: **1. You cannot tell which case failed.** The report says `Input (7)` failed. Nothing about `Input (7)` tells you which of your inputs it was; you have to count elements in the source list and hope the order is what you think it is. **2. Index-suffixed names are positional, not semantic.** Insert a case at the front of the list and every subsequent suffix shifts by one. Yesterday's `Input (3)` is today's `Input (4)`. Anything that remembers names across runs — CI test history, flaky-test tracking, quarantine lists, a saved IDE run configuration — is now pointing at a different case, silently. **3. Name-based selection stops working.** Kotest can filter tests by name/path. If ten cases share a base name, you cannot address one of them; you either run all ten or none, which is precisely the capability data tests were supposed to give you over a hand-written loop. ## The fix is upstream, not the mode Setting `Error` does not make names good; it makes bad names loud. The actual fix is the naming machinery: - make the input a **data class** so its `toString()` names it; - implement **`WithDataTestName`** and return something that identifies the case; - annotate a class with a good hand-written `toString()` as **`@IsStableType`**; - pass a **`nameFn`** that includes every field that varies across the case list; - or use the **map overload** and write the names yourself — remembering that duplicate map keys drop a case entirely before Kotest is involved, which is a worse failure than a collision because nothing warns at all. A useful review heuristic: if two cases in a list would produce the same sentence when read aloud, either they are genuinely the same case (delete one) or the name is missing the dimension that distinguishes them. ## Scope of uniqueness Uniqueness is required **within a scope**, not globally. Two different `context` blocks may each contain a case named `"empty input"` without any collision, because their full paths differ. This is another argument for wrapping `withData` in a named container: it both documents the behaviour under test and widens the space of acceptable case names. ## Interaction with nested `withData` In a nested (cross-product) data test, each level names its own node and the full path is the composition. Collisions therefore only need to be resolved among the siblings at one level. A cross-product with a poorly-named outer level produces `Config`, `Config (1)`, `Config (2)` as the containers, and every child underneath inherits that ambiguity in its path — so fix the outer level first; it multiplies. ## How to say it in an interview "Kotest needs unique names per scope, so `DuplicateTestNameMode` decides — the 5.x default `Warn` appends an index and keeps the build green, `Error` fails it. Duplicates in `withData` mean the stable-identifier logic fell back to a type name, or a `nameFn` dropped the distinguishing field. I'd set `Error` in project config on a data-heavy suite, because index-suffixed names are positional: they shift when the list changes and break re-runs, filtering, and CI history."

  • Why is `DuplicateTestNameMode.Error` often the right project-wide setting for a data-heavy suite?
    Because the default `Warn` keeps the build green while producing a report you cannot act on — a failing `Input (7)` does not tell you which input failed, and the index shifts whenever the case list changes. `Error` converts a silent reporting defect into a build failure at the moment someone introduces it, when the fix (a data class, a better `nameFn`) is cheap and obvious.
  • Do two tests in different `context` blocks with the same name collide?
    No — uniqueness is required within a scope, and the full path of each node includes its containers. Two sibling containers may each hold a case called "empty input" without any conflict. This is a practical reason to wrap `withData` in a named container: it documents what is being parameterised and gives short case names more room to be distinct.

saying these in an interview costs you the question

  • "Duplicate test names fail the build by default" — the Kotest 5.x default is Warn, which disambiguates and stays green.
  • "Names have to be globally unique across the project" — uniqueness is per scope; the container path disambiguates.
  • "Setting DuplicateTestNameMode.Error fixes the names" — it only makes bad names loud; the fix is in the name derivation.
  • "The (1), (2) suffixes are stable identifiers I can filter on" — they are positional and shift when the case list changes.
  • "The map overload just warns on duplicate keys" — the duplicate collapses inside the map before Kotest sees it, so a case silently disappears.

context