Explain how Kotest's table(), row(), and forAll work together for table-driven testing. Show a multi-column example.
answer
- row() builds a tuple; table() + headers groups them
- forAll { a, b, expected -> } destructures each row
- Lambda arity == column count
- io.kotest.data, not io.kotest.property
- forNone is the negative variant
basics
~10 sYou build a table from rows, where each row holds several values (the inputs and expected output). Then forAll runs your check once per row, passing that row's values into your lambda.
solid answer
~40 sKotest's table-testing API (in io.kotest.data) is built from three pieces: row(...) packages a tuple of values for one case, table(headers(...), row(...), row(...)) collects those rows with column headers, and forAll(table) { a, b, ... -> } runs the assertion block once per row, destructuring each row into the lambda's parameters. row() supports up to many columns (Row1..RowN), and forAll's lambda arity must match the column count. Unlike withData (which takes a single value type per row), the table form is ideal for multi-column input->expected cases because each column is a named, separately-typed parameter. forAll continues through all rows and aggregates failures, reporting which row(s) failed. There is also a forNone variant. Headers are for documentation/output readability.
code
kotlin · 9 linestest("max") {
table(
headers("x", "y", "max"),
row(1, 2, 2),
row(5, 3, 5),
).forAll { x, y, max ->
maxOf(x, y) shouldBe max
}
}go deeper
Recognizes row/table/forAll as the table-testing trio and can read a simple example.
Writes a correct multi-column table, matches lambda arity, and knows forAll runs in one test body.
Distinguishes data.forAll from property.forAll, uses forNone, and picks table vs withData per situation.
Sets team conventions for when table tests improve readability vs. when properties or parameterized data classes scale better.
## The three building blocks Kotest's classic table-driven API lives in `io.kotest.data` and composes three functions: - **`row(...)`** — wraps one case's values into a typed tuple (`Row1<A>`, `Row2<A,B>`, ... up to many columns). - **`table(headers(...), row(...), row(...))`** — collects rows under named column headers. `headers("a", "b", "expected")` are labels used for readable output. - **`forAll(table) { a, b, expected -> ... }`** — executes the block **once per row**, destructuring each row into the lambda parameters. The lambda's parameter count must equal the number of columns. ## Multi-column example ```kotlin import io.kotest.core.spec.style.FunSpec import io.kotest.data.forAll import io.kotest.data.headers import io.kotest.data.row import io.kotest.data.table import io.kotest.matchers.shouldBe class AddTest : FunSpec({ test("addition table") { table( headers("a", "b", "sum"), row(1, 2, 3), row(0, 0, 0), row(-1, 1, 0), ).forAll { a, b, sum -> (a + b) shouldBe sum } } }) ``` Each `row(a, b, sum)` is a `Row3<Int,Int,Int>`. `forAll` destructures it into `(a, b, sum)`. ## forAll vs forNone - **`forAll`** asserts the block passes for **every** row. - **`forNone`** asserts it passes for **no** row (useful for negative tables). Both iterate all rows and **aggregate failures**, telling you which row(s) and values failed rather than stopping at the first. ## Relationship to withData - `forAll(table)` shines for **multi-column, differently-typed** input/expected matrices, with explicit headers. - `withData` is simpler when each case is a **single value** (often a data class bundling fields) and you want one **generated test per row** in the report. ## Gotchas - The `forAll`/`forNone` here are **table** functions from `io.kotest.data` — **distinct** from the property-testing `forAll` in `io.kotest.property` that generates random inputs. Import the right one. - The lambda **arity must match** the row width or it won't compile. - Table `forAll` runs **inside a single test** body; it does not create one Kotest test per row (that is `withData`'s behavior).
- Does table forAll create one Kotest test per row?No. The table forAll iterates all rows inside a single test body and aggregates failures. To get one generated test per row, use withData instead.
- How do you handle the naming clash with property-based forAll?They live in different packages: io.kotest.data.forAll (tables) vs io.kotest.property.forAll (random properties). Import the correct one or fully-qualify.
saying these in an interview costs you the question
- Confusing io.kotest.data.forAll with the property-testing forAll
- Saying table forAll generates one test per row (it does not)
- Mismatching lambda parameter count with column width
- Thinking headers are required for correctness rather than readability
- Claiming forAll stops at the first failing row