Explain the difference between Arb and Exhaustive generators in Kotest. When would you choose each, and how do you build custom or composed generators?
answer
- Arb = random + injected edge cases (sampling)
- Exhaustive = finite set, each value once
- map/filter/flatMap to transform
- Arb.bind for data classes; Arb.of/choice for alternatives
- arbitrary { rs -> } for full custom + reproducible
basics
~10 sArb makes random values (plus some edge cases) and suits large input spaces. Exhaustive lists every value and suits small, finite spaces. You build custom ones by mapping, filtering, or combining existing generators.
solid answer
~40 s`Arb<T>` produces random samples and deterministically injects edge cases (0, empties, MIN/MAX); it samples a large space, so it never guarantees full coverage. `Exhaustive<T>` enumerates a fixed finite set (booleans, enum values, a small list) and tries each exactly. Use Arb for ints, strings, large collections; use Exhaustive when every case matters and the set is small (enum states, flags). You compose generators functionally: `Arb.int().map { it * 2 }`, `Arb.string().filter { it.isNotBlank() }`, and `Arb.bind(genA, genB) { a, b -> Pair(a, b) }` to build data classes. `Arb.choice`/`Arb.of` pick among alternatives; `.orNull()` injects nulls. You can mix an Arb and an Exhaustive in one checkAll, and Kotest takes their cartesian-ish product. Custom Arbs control edge cases via `arbitrary { rs -> … }` using the RandomSource.
code
kotlin · 19 linesimport io.kotest.property.Arb
import io.kotest.property.arbitrary.*
import io.kotest.property.exhaustive.*
import io.kotest.property.checkAll
enum class Role { ADMIN, USER, GUEST }
data class Account(val name: String, val role: Role)
val accountArb = Arb.bind(
Arb.string(1..10).filter { it.isNotBlank() },
Arb.enum<Role>()
) { name, role -> Account(name, role) }
suspend fun run() {
// mix Arb with an Exhaustive role set
checkAll(accountArb, Exhaustive.enum<Role>()) { acc, role ->
// ... property over acc and every role
}
}go deeper
Knows Arb is random and Exhaustive is a fixed set, and can pick the obvious one for booleans vs ints.
Composes generators with map/filter/bind, builds a data-class Arb, and understands edge-case injection.
Avoids filter-driven discards via bounded Arbs, uses arbitrary { rs } for reproducible custom generators, and mixes Arb/Exhaustive deliberately.
Designs a shared generator library for domain types, balancing coverage, generation cost, and shrink quality across the test suite.
## Two generator families ### `Arb<T>` — arbitrary (random + edge cases) Produces an effectively unbounded stream of **random** values, but first deterministically yields **edge cases** (e.g. `Arb.int()` injects `0`, `Int.MIN_VALUE`, `Int.MAX_VALUE`; `Arb.string()` injects empty/blank). Because it samples, it cannot prove full coverage — only that the property survived N tries. Common built-ins: `Arb.int()`, `Arb.long()`, `Arb.double()`, `Arb.string()`, `Arb.boolean()`, `Arb.uuid()`, `Arb.enum<MyEnum>()`, and collection arbs `Arb.list(elementArb)`, `Arb.set(...)`, `Arb.map(...)`. ### `Exhaustive<T>` — every value, once Enumerates a **finite** set; each value is tried. Examples: `Exhaustive.boolean()`, `Exhaustive.enum<MyEnum>()`, `Exhaustive.collection(listOf("a", "b", "c"))`, `Exhaustive.ints(0..10)`. Choose it when the space is small and you want certainty. ## Choosing between them | Use Arb when | Use Exhaustive when | |---|---| | Space is huge (ints, strings, big lists) | Space is small + finite (enums, flags) | | Sampling + edge cases is enough | You need every case covered | ## Composing generators Generators are functorial/monadic, so you transform them: ```kotlin import io.kotest.property.Arb import io.kotest.property.arbitrary.* val evens: Arb<Int> = Arb.int().map { it * 2 } val nonBlank: Arb<String> = Arb.string().filter { it.isNotBlank() } val nullableName: Arb<String?> = Arb.string().orNull() // flatMap when the next gen DEPENDS on a value val listOfSize: Arb<List<Int>> = Arb.int(1..5).flatMap { n -> Arb.list(Arb.int(), n..n) } ``` ### Building a data class generator with bind ```kotlin data class User(val id: Int, val name: String, val active: Boolean) val userArb: Arb<User> = Arb.bind( Arb.int(1..1_000), Arb.string(1..20), Arb.boolean() ) { id, name, active -> User(id, name, active) } ``` `Arb.bind` (arities up to many params) combines independent generators. `Arb.choice(a, b, c)` and `Arb.of(v1, v2)` select among options. `Arb.constant(x)` always yields `x`. ### Fully custom Arb via the `arbitrary { }` builder ```kotlin val positiveEven = arbitrary { rs -> // rs: RandomSource val v = rs.random.nextInt(1, 1000) v * 2 } ``` The block receives a `RandomSource`, making generation reproducible from a seed. ## Mixing Arb and Exhaustive A single `checkAll(Arb.int(), Exhaustive.boolean()) { i, b -> … }` is legal; Kotest iterates appropriately, effectively crossing the exhaustive set against sampled values. ## Edge cases and coverage `Arb` edge-case injection is the main reason PBT finds overflow/empty bugs. If you `map`/`filter`, you can accidentally filter out edge cases or create rejection slowdowns — prefer constructing constrained ranges (`Arb.int(0..100)`) over heavy `filter`.
- Why prefer Arb.int(0..100) over Arb.int().filter { it in 0..100 }?filter rejects most samples, wasting iterations and risking 'too many discards' failures; a bounded Arb generates valid values directly and is far faster.
- How do you generate nullable values?Call .orNull() on an Arb (optionally with a nullProbability), which injects nulls alongside generated values.
saying these in an interview costs you the question
- Uses Exhaustive on an effectively infinite space like Arb-style ints
- Heavy reliance on filter, causing discard/slowness problems
- Thinks Arb guarantees full coverage of the input space
- Doesn't know Arb.bind / map / flatMap exist for composition