How do you drive a Kotest property test with several generators at once, and what happens if you call Kotest's `checkAll<A, B>()` with type arguments but no generator arguments?
answer
- arity overloads: N gens → N params
- positional binding, tuples not cross product
- checkAll<A, B>() → Arb.default<T>() reflection
- runtime error when no default exists
- past the arity limit → generate a data class
basics
~20 sPass one generator per lambda parameter — checkAll(Arb.int(), Arb.string()) { a, b -> ... } — using the arity overloads. With only type arguments, Kotest resolves a default Arb for each type reflectively and errors if a type has none.
solid answer
~50 sKotest's `checkAll`/`forAll` come in arity overloads: you pass N generators and the lambda receives N parameters, positionally matched. Each iteration draws one value from each generator, so the parameters vary together rather than as a cartesian product you control. You can mix generator kinds in one call — an `Arb` for the random dimension and an `Exhaustive` for a small enumerated one — because both are `Gen`s. There is also a type-inferred form: `checkAll<Int, String> { a, b -> … }`. Kotest resolves a default `Arb` per type via `Arb.default<T>()`; that works for the standard scalar types and fails with a "no generator found" style error for types it cannot build. It is convenient for quick laws, but I prefer explicit generators because the value domain becomes visible and constrainable. The overloads stop at a fixed arity. Beyond it — or when the parameter list stops being readable — bundle the inputs into a data class and generate that one value instead.
code
kotlin · 3 linescheckAll(Exhaustive.enum<Currency>(), Arb.int(0..10_000)) { currency, amount ->
format(currency, amount) shouldStartWith currency.symbol
}go deeper
Show that you can write a two-generator property and know the parameters line up positionally with the generators.
Explain the tuple-per-iteration sampling model, that Arb and Exhaustive can be mixed because both are Gens, and what the type-inferred checkAll<A, B>() form actually does.
Add the operational judgement: explicit generators for domain visibility, the runtime failure mode of reflective defaults, and collapsing wide parameter lists into a generated data class.
Discuss it as suite-level convention — how properties stay reviewable as input models grow, and how combination coverage is designed deliberately rather than hoped for.
## One generator per parameter Kotest's property entry points are overloaded by arity. Each overload takes N generators and a lambda of N parameters: ```kotlin checkAll(Arb.int(), Arb.string(), Arb.boolean()) { n, s, flag -> // n: Int, s: String, flag: Boolean } ``` The binding is **positional**: the first generator feeds the first lambda parameter, and so on. Each iteration draws one value from every generator, so with 1000 iterations you get 1000 *tuples*, not 1000 values per dimension and not an exhaustive cross product of the dimensions. That matters when you reason about coverage: adding a fourth parameter does not multiply the work, it just makes each sample richer — and makes each sample less likely to hit any particular combination you care about. ## Generators, not just Arbs The parameters are typed as `Gen`, the common supertype of `Arb` (random plus injected edge cases) and `Exhaustive` (a finite domain walked in full). So you can mix them freely in one call: ```kotlin checkAll(Exhaustive.enum<Currency>(), Arb.int(0..10_000)) { currency, amount -> format(currency, amount) shouldStartWith currency.symbol } ``` That combination — enumerate the small dimension, randomise the large one — is the everyday shape of a multi-generator property. ## The type-inferred form There is a second family that takes **no** generator arguments and infers them from reified type parameters: ```kotlin checkAll<Int, String> { n, s -> (n.toString() + s).length shouldBe n.toString().length + s.length } ``` Under the hood Kotest asks `Arb.default<T>()` for each type parameter. Defaults exist for the standard scalar types (the numeric types, `Boolean`, `String`, and similar). If a type has no registered default, the call fails at runtime with an error saying no generator could be found for it — a runtime failure, not a compile error, which is the main reason to be wary of the form. The convenience is real for quick algebraic laws. The cost is that the value domain is invisible: `checkAll<Int, String>` silently accepts the full `Int` range and the default `String` shape (including empty strings and whatever the default character set is). When a property depends on the domain being constrained — a positive quantity, a bounded list, an ASCII identifier — the explicit form is the one that lets you say so. ## Running out of arity The overload set is finite. Two things happen as you approach it: you run out of overloads, and long before that the lambda's parameter list becomes unreadable (`{ a, b, c, d, e, f -> … }` with no names carrying meaning). The idiomatic fix is the same in both cases: define a data class for the input tuple and generate *it* with a single generator, so the property takes one well-named parameter and each field is generated by its own constrained generator. (Composing generators into a data class is the `Arb.bind` family; that composition surface is a topic in its own right.) ## Iterations across arities All arities share the same configuration surface: an explicit iteration count, or a `PropTestConfig` as the first argument. Nothing about adding generators automatically increases the iteration count you asked for, so if a property has several dimensions and you care about combination coverage, you must raise iterations yourself — or restructure the property so the combinations that matter are enumerated by an `Exhaustive` rather than left to chance. ## Practical guidance - Prefer explicit generators; reach for the type-inferred form only for throwaway laws over scalars. - Order parameters so the reader can follow the story of the property (input, then configuration, then flags). - Mix `Exhaustive` for the small categorical dimension and `Arb` for the open-ended one. - The moment the parameter list stops reading like a sentence, collapse it into a generated data class. - Remember the sampling model: tuples drawn together, not a cross product — coverage of combinations is your responsibility, not the framework's.
- With `checkAll(Arb.int(), Arb.int())` and 1000 iterations, how many combinations of the two dimensions are tested?One thousand — one tuple per iteration, with both values drawn for that iteration. It is not a cross product of 1000 by 1000. If you need particular combinations covered deterministically, enumerate the small dimension with an `Exhaustive` or raise the iteration count, rather than assuming the framework multiplies the dimensions for you.
- Why might a team ban the `checkAll<A, B>()` type-inferred form in code review?Because the generator, and therefore the value domain, becomes invisible: the property silently runs over the whole `Int` range or the default `String` shape, and nobody reviewing the test can see or constrain that. It also turns a missing generator into a runtime failure for types without a registered default, instead of a compile-time one.
- You have eight inputs to a property. What is the idiomatic Kotest approach?Bundle them into a data class and generate that single value, giving each field its own suitably constrained generator, then take one well-named parameter in the property lambda. It sidesteps the arity limit, makes the property readable, and keeps the domain of each field explicit and reviewable.
saying these in an interview costs you the question
- Assuming multiple generators produce a cartesian product of all their values
- Believing `checkAll<A, B>()` works for any type, including your own data classes, without configuration
- Thinking adding another generator automatically multiplies the iteration count
- Saying `Exhaustive` cannot be mixed with `Arb` in the same property call
- Passing generators in an order that does not match the lambda parameters and expecting Kotest to match by type