skip to content

When using withData with a data class per case, how do you produce readable test names, and what are the naming overloads?

level: middleimportance: should knowfreq 40%

answer

  1. Default name = toString()
  2. Map keys -> names; values -> lambda
  3. nameFn = { it -> "..." } overload
  4. Data class destructuring in lambda via componentN
  5. Avoid duplicate/opaque names

basics

~20 s

By default each generated test is named from the value's toString(). With a data class you get the auto-generated toString, which is usually readable. If you want nicer names, give withData a Map (keys become names) or a function that builds the name from each value.

solid answer

~40 s

withData names each generated test from the element's toString() by default. For a data class, Kotlin's auto-generated toString() (e.g. Case(a=1, b=2, expected=3)) is often readable enough. To control names you have overloads: pass a Map<String, T> so the map keys become the test names; or use the nameFn overload, withData(nameFn = { case -> "..." }, first, second, ...) or withData(nameFn, collection), which computes a name from each element. There are also overloads taking a first element plus varargs, an Iterable, or a Sequence. Good naming matters because withData generates one test per row and duplicate or opaque names hurt the report and can collide. A common pattern is a data class holding inputs and expected output, with a nameFn that summarizes the case.

code

kotlin · 9 lines
kotlin
withData(
    mapOf(
        "empty" to "",
        "single" to "a",
        "long" to "abcde",
    )
) { input ->
    input.length shouldBe input.count { true }
}

go deeper

for a junior

Knows names come from toString() and a data class usually reads fine.

for a middle

Uses Map and nameFn overloads and destructures data-class cases in the lambda.

for a senior

Designs case data classes plus nameFn for readable, collision-free reports and explains componentN destructuring.

for a principal

Standardizes a team pattern (case data class + nameFn) so data-driven test output is self-documenting across the codebase.

## Default naming `withData` derives each generated test's name from the element's **`toString()`**. For primitives that's the literal value; for a **data class**, Kotlin's compiler-generated `toString()` prints all properties (`Case(a=1, b=2, expected=3)`), which is usually descriptive. ## Overloads for explicit names Kotest offers several `withData` shapes: - **Varargs:** `withData(t1, t2, t3) { ... }` — names from `toString()`. - **Iterable/Sequence:** `withData(listOf(...)) { ... }` and `withData(sequenceOf(...)) { ... }`. - **Map:** `withData(mapOf("adds positives" to Case(1,2,3))) { ... }` — **map keys become test names**, values are passed to the lambda. - **nameFn:** `withData(nameFn = { c -> "${c.a}+${c.b}=${c.expected}" }, c1, c2) { ... }` (also a collection overload) — compute the name from each element. ## Recommended data-class pattern ```kotlin import io.kotest.core.spec.style.FunSpec import io.kotest.datatest.withData import io.kotest.matchers.shouldBe data class Case(val a: Int, val b: Int, val expected: Int) class AddTest : FunSpec({ context("addition") { withData( nameFn = { "${it.a} + ${it.b} == ${it.expected}" }, Case(1, 2, 3), Case(0, 0, 0), Case(-1, 1, 0), ) { (a, b, expected) -> (a + b) shouldBe expected } } }) ``` Note the lambda **destructures** the data class via `(a, b, expected)`, which works because `data class` generates `componentN()` operators. ## Why naming matters - `withData` creates **one test per row**; clear names make the report readable. - **Duplicate names** are problematic — if several elements share a `toString()`, distinguish them with a `nameFn` or a Map. - Opaque defaults (e.g. a class without a custom `toString`) yield names like `Case@1a2b3c`; override `toString()` or use `nameFn`. ## Key takeaway Prefer a small **data class per case** plus a **`nameFn`** (or `Map`) so every generated test reads like a sentence.

  • Why might a plain class (not data class) give bad test names?
    Without a custom toString(), its default is the identity hash (Foo@1a2b3c), which is opaque. Use a data class, override toString(), or supply a nameFn/Map.
  • How does the lambda destructure a data class case?
    data class generates componentN() operators, so withData(...) { (a, b, expected) -> } destructures the element into named locals.

saying these in an interview costs you the question

  • Believing you cannot customize generated test names
  • Using a plain class with no toString and getting unreadable names
  • Ignoring duplicate-name collisions across rows
  • Thinking Map values become names (keys do)
  • Not knowing destructuring relies on componentN() from data class

context