skip to content

Composing Generators (Arb.bind)

Real properties need generators for domain types, built by composing primitives with bind, map, flatMap, and filter. This is the differentiating skill interviewers look for — turning Arb.int and Arb.string into an Arb<Order> with valid invariants.

on this pageshow

explore

questions

6

In Kotest's property testing, how do you build a generator for a data class out of generators for its individual fields, and what does Kotest's Arb.bind give you that constructing the object by hand inside the test body does not?

level: middleimportance: must knowfreq 45%

answer

  1. Arb.bind(a, b) { … } → Arb<T>
  2. positional lambda, not named params
  3. hand-rolled field = no shrink, no edge cases, no replay
  4. nest binds for aggregates
  5. dependent fields → flatMap / arbitrary { }

basics

~20 s

Pass one Arb per constructor parameter to Kotest's Arb.bind, with a lambda that assembles the object: Arb.bind(Arb.string(), Arb.int(1..120)) { n, a -> Person(n, a) }. The result is a real Arb, so its fields keep their edge cases and shrink independently.

solid answer

~50 s

`Arb.bind` in kotest-property takes N component generators plus a builder lambda and returns an `Arb<T>`: ```kotlin val personArb = Arb.bind(Arb.string(minSize = 1, maxSize = 20), Arb.int(1..120)) { name, age -> Person(name, age) } ``` The alternative — taking `Arb.string()` in `checkAll` and building the rest of the object inside the test body from hand-rolled values — makes those fields invisible to the framework. Kotest can only vary, report and shrink what a generator produced, so hand-built fields never get edge cases, never shrink, and never appear in the counterexample it prints. With `bind`, each component contributes its own edge cases and its own shrinker, so a failure shrinks toward a minimal `Person` rather than a random one. `bind` nests: build an `Arb<Address>` and feed it into `Arb.bind` for `Arb<Customer>`. Overloads exist only up to a fixed arity; for very wide constructors, or when one field depends on another, use the `arbitrary { }` builder with `.bind()` instead.

code

kotlin · 10 lines
kotlin
data class Person(val name: String, val age: Int)

val personArb: Arb<Person> = Arb.bind(
   Arb.string(minSize = 1, maxSize = 20),
   Arb.int(1..120),
) { name, age -> Person(name, age) }

checkAll(personArb) { person ->
   person.age shouldBeGreaterThan 0
}

go deeper

for a junior

Be able to write the call: one Arb per constructor parameter plus a lambda that builds the object, and know the result is itself an Arb you pass to checkAll.

for a middle

Explain why generated-not-improvised matters — shrinking, edge cases, printed counterexamples, seed replay — and show nesting for aggregates.

for a senior

Add the limits: fixed arity, independent sampling, invariants and init-block validation, and when you switch to flatMap or the arbitrary { } builder.

for a principal

Frame it as fixture design: a named arb per domain type, composed upward, constrained so generators can only build legal values, shared across specs instead of re-declared.

## The problem bind solves Property-based testing feeds generated values into a test body many times. In Kotest the generator type is `Arb<A>` (arbitrary) from the `kotest-property` module. Kotest ships generators for primitives and collections — `Arb.string()`, `Arb.int(1..120)`, `Arb.boolean()`, `Arb.list(...)` — but your domain is made of data classes, not primitives. `Arb.bind` is the standard way to lift field generators into an object generator. ```kotlin data class Person(val name: String, val age: Int) val personArb: Arb<Person> = Arb.bind( Arb.string(minSize = 1, maxSize = 20), Arb.int(1..120), ) { name, age -> Person(name, age) } ``` The signature is: N generators, then a lambda receiving one sampled value from each, returning the assembled object. Positional order matters — the lambda parameters line up with the generator arguments, not with the constructor's parameter names, so swapping two same-typed generators silently produces a wrong-but-compiling generator. ## Why not build the object inside the test body A very common shortcut is: ```kotlin checkAll(Arb.string()) { name -> val person = Person(name, Random.nextInt(1, 120)) // hidden input ... } ``` This compiles and it does test something, but the age is invisible to the framework, and that has four concrete consequences: 1. **No reporting.** When the property fails, Kotest prints the arguments it generated. The hand-rolled age is not one of them, so the printed counterexample does not describe the failing case. 2. **No shrinking.** Shrinking is the search for a smaller failing input; Kotest can only shrink values that came out of an `Arb`. A hand-rolled field stays at whatever random value it had. 3. **No edge cases.** Kotest's generators mix in known-nasty values (empty string, zero, boundary integers) alongside random samples. Hand-rolled values get none of that. 4. **No replay.** Kotest's seed pins its own random source. `Random.nextInt` outside that source is not reproduced by re-running with the printed seed, so the test becomes non-reproducible. Everything you want generated should therefore come from a generator. ## Composition and nesting `bind` returns a plain `Arb`, so it composes like any other: ```kotlin val addressArb = Arb.bind(Arb.string(), Arb.string()) { street, city -> Address(street, city) } val customerArb = Arb.bind(personArb, addressArb, Arb.boolean()) { p, a, active -> Customer(p, a, active) } ``` This is how you build a generator for a deep aggregate: a small named `Arb` per type, then compose upward. Named, reusable arbs in a test fixture beat re-declaring the same shape in five specs. ## Limits to know - **Arity.** The `bind` overloads stop at a fixed number of components. A constructor wider than that needs the `arbitrary { }` builder, where you call `.bind()` on each generator inside the block. - **Independent fields only.** `bind` samples every component independently before your lambda runs. If field B must depend on the sampled value of field A (`endDate` after `startDate`, list index within list size), `bind` cannot express it — use `flatMap` or `arbitrary { }`. - **Invariants.** `bind` will happily build objects your domain considers illegal. Enforce the invariant by constructing valid values (derive the dependent field), not by wrapping the whole generator in `filter`, which throws work away. - **Init blocks that throw.** If the data class validates in `init`, an out-of-range component makes the generator itself throw mid-run. Constrain the component generator (`Arb.int(1..120)` rather than `Arb.int()`) so the generator can only produce constructible values. ## What a good answer sounds like Name the API, show the positional lambda, then justify it in terms of shrinking, edge cases, reporting and reproducibility — those four are the reason the framework wants your inputs declared as generators rather than improvised in the body. Mention nesting for aggregates, and that dependent fields need `flatMap`/`arbitrary { }` instead.

  • Your data class validates its arguments in an init block and the property run blows up before the test body even executes. What went wrong?
    A component generator produced a value the constructor rejects, so the exception comes from the generator's builder lambda, not from your assertion. Fix it by constraining the component — `Arb.int(1..120)` instead of `Arb.int()`, `Arb.string(minSize = 1)` instead of an unbounded string. Filtering the assembled object afterwards also works but wastes samples and skews the distribution.
  • Two of the constructor's parameters are both Strings. What is the failure mode with Arb.bind?
    The lambda parameters match the generator arguments positionally, so swapping the two generators still type-checks and still compiles. You end up generating e-mails into the name field with no compiler error. Guard against it by naming the arbs (`emailArb`, `nameArb`) and by naming the constructor arguments inside the lambda body.

saying these in an interview costs you the question

  • Generating one field with an Arb and improvising the rest inside the test body with kotlin.random
  • Thinking Arb.bind can express a dependency between two fields
  • Assuming the lambda parameters bind by constructor parameter name rather than by position
  • Wrapping the whole bound generator in filter to enforce an invariant that could just be constructed correctly
  • Believing hand-rolled values still appear in Kotest's printed counterexample

context

open as a page

Kotest's Arb type offers map, flatMap and filter. Explain what each does to a generator, and why filter is the one to be careful with.

level: middleimportance: should knowfreq 40%

basics

~20 s

map transforms each sampled value (Arb<A> to Arb<B>). flatMap feeds a sampled value into a function that picks the next generator, so it expresses dependent values. filter re-samples until a predicate passes — it wastes samples, can skew the distribution and can stall when matches are rare.

open as a page

When would you reach for Kotest's arbitrary { } generator builder with its .bind() shorthand instead of Arb.bind, and what do you have to think about when you do?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Use kotest-property's arbitrary { } builder when the object cannot be assembled from independently sampled fields: many parameters, fields that depend on each other, or conditional structure. Inside the block, calling .bind() on an Arb yields a sampled value. Watch shrinking — supply a Shrinker if you need it.

open as a page

You need a Kotest generator that produces values from several alternative sources — say valid IDs from one generator and malformed ones from another. What does kotest-property offer for that, and how do Arb.merge, Arb.choice and Arb.choose differ?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

Kotest's Arb.choice(a, b, ...) picks one of the supplied generators uniformly per sample; Arb.choose(3 to a, 1 to b) picks by integer weight; and a.merge(b) combines two generators of compatible type by alternating between them. All three return a normal Arb you can compose further.

open as a page

kotest-property offers a reflective form of Arb.bind that builds a generator for a class without you naming any field generators. How does it work, and why would you not use it for your core domain types?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

The reified Arb.bind<T>() form inspects the class's primary constructor and resolves a generator for each parameter from Kotest's built-in defaults. It is convenient for throwaway or wide DTOs, but it ignores your domain constraints and fails at runtime for parameter types it has no default for.

open as a page

Your team's Kotest property tests each declare their own generators inline, and the same domain objects are generated five slightly different ways across the suite. How would you organise generators for a real domain model, and what conventions would you set?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Treat generators as a shared fixture library: one named Arb per domain type in a test-fixtures source set, composed upward with Arb.bind, constrained so every sample is legal by construction, with per-test narrowing done by composition rather than by copying and filtering.

open as a page