skip to content

What does Kotest's `Arb<A>.orNull()` produce, and how would you use it to exercise a nullable API contract without swamping the run with nulls?

level: middleimportance: nice to knowfreq 22%

answer

  1. orNull: Arb<A> → Arb<A?>
  2. nullProbability tunes the rate
  3. 0.0 = never, high = null-dominated
  4. composes into data-class field generators
  5. don't !! or skip the null branch

basics

~10 s

orNull turns an Arb<A> into an Arb<A?> that emits null with a small probability alongside the underlying values. The nullProbability parameter tunes that rate, so you can make nulls rare, frequent, or effectively absent.

solid answer

~50 s

`Arb<A>.orNull()` lifts a generator into its nullable form: the resulting `Arb<A?>` mostly yields values from the source generator and occasionally yields `null`. The rate is controlled by the `nullProbability` parameter, which defaults to a small value — enough that `null` appears early in a run rather than by luck, but not so much that most iterations test only the null path. ```kotlin checkAll(Arb.string(1..20).orNull(nullProbability = 0.2)) { note -> render(note) shouldNotBe null } ``` It is the right tool for optional fields: a nullable request field, an optional configuration value, a lookup that may miss. Because `null` participates as a value of the generator, it also flows into composed generators, so a data class built from field generators automatically explores the combination of "note is null while quantity is at its boundary" without you enumerating it. Tune the probability by what you are testing: raise it when the null path is the interesting one, lower it when null is a rare production case.

code

kotlin · 4 lines
kotlin
checkAll(Arb.string(minSize = 1, maxSize = 20).orNull(nullProbability = 0.2)) { note ->
    val rendered = render(note)
    if (note == null) rendered shouldContain "(no note)" else rendered shouldContain note
}

go deeper

for a junior

Know that orNull produces a nullable generator that mixes in null occasionally, and that the property body must handle both branches.

for a middle

Explain the probability parameter, how to choose it from what the property is claiming, and that a nullable generator composes into larger value generators.

for a senior

Add the judgement: null coverage is exploratory not guaranteed, two-branch properties often want splitting, and orNull should reflect a real optional field in the contract.

for a principal

Position generators as the executable statement of the input contract, with optionality modelled explicitly so absence combinations get explored instead of assumed away.

## Lifting a generator into nullable Kotlin's type system distinguishes `A` from `A?`, and so does Kotest's generator API. `Arb<A>.orNull()` returns an `Arb<A?>`: a generator that draws from the source generator most of the time and yields `null` the rest of the time. ```kotlin val maybeName: Arb<String?> = Arb.string(minSize = 1, maxSize = 20).orNull() ``` The frequency is controlled by the `nullProbability` parameter. The default is deliberately small — `null` should be visible early in a run without dominating it. Setting it to `0.0` gives you effectively no nulls (at which point you should just use the source generator), and setting it high makes the null path the main subject of the property. ## Why this is more than a convenience The null case is the archetypal cheap bug: a field that is optional in the schema, required in the code, and never exercised because every hand-written test fixture fills it in. Generating nullability puts that case into the run automatically, and — crucially — puts it into *combination* with other generated values. A property over a data class whose optional fields use `orNull` explores "note absent while quantity is at its maximum" and "both optional fields absent" without anyone writing those fixtures. It also composes: an `Arb<A?>` is an ordinary generator, so it can be mapped, constrained, or used as a field generator when building a larger value. That is why nullable-aware generators pay off most in data-class generation, where the number of null/non-null combinations grows combinatorially and hand-enumeration stops being practical. ## Choosing the probability Think about what the property is claiming. - **Null is a rare production case** and the property is mostly about the happy path: keep the probability low. You still get null coverage, but most iterations exercise real values. - **Null is the interesting case** — you are testing default substitution, a fallback, or an error message: raise the probability so a meaningful fraction of iterations exercise it. Better still, consider a dedicated property or example test for the null path where the assertion can be specific. - **Null must never occur** in the domain: do not use `orNull` at all. Generating a value your contract forbids and then asserting around it tests nothing useful. A subtlety worth stating: a low probability plus a low iteration count can mean zero nulls in a given run. If the null case genuinely must be covered every time, cover it with an example test as well — property generation gives you exploration, not a guarantee that a particular value was drawn. ## Assertions must handle both branches Because the generated type is `A?`, the property body sees a nullable value and Kotlin will force you to deal with it. Resist the temptation to `!!` it or to skip the iteration with an early return when it is null: both convert a generated case back into an untested one. Write the assertion so it states the true property for both branches — for example, "rendering never returns null", or "the output contains the note when present and the placeholder when absent". ```kotlin checkAll(Arb.string(1..20).orNull()) { note -> val rendered = render(note) if (note == null) rendered shouldContain "(no note)" else rendered shouldContain note } ``` If the property becomes an awkward two-branch conditional, that is often a hint that there are really two properties — one for present and one for absent — and splitting them makes both assertions sharper. ## Relationship to the rest of the generator surface `orNull` sits alongside the other domain-shaping tools: ranges and sizes constrain *which* values appear, `orNull` decides whether the absence of a value is in the domain at all. Together they are how a generator becomes an executable statement of the input contract: "a name of 1 to 20 alphanumeric characters, a quantity from 1 to 99, and an optional note". Reviewing generators with that sentence in mind is the fastest way to spot a property testing the wrong domain — including the very common case of a field that is nullable in the data class but was never generated as null.

  • Your property uses `orNull` with a low probability and 20 iterations. Is null guaranteed to be tested?
    No. With a low probability and few iterations, a run can easily draw no nulls at all. Property generation explores the domain, it does not promise that a particular value appeared. If covering the null path is a hard requirement, add a deterministic example test for it, or raise the probability and iteration count so the case is reliably reached.
  • What is wrong with calling `!!` on the generated value inside a property that uses `orNull`?
    It converts every null draw into a thrown exception or a meaningless failure instead of testing the behaviour you actually care about. If null is in the generated domain, the property must state what should happen when it occurs; if null is not supposed to be in the domain, drop `orNull` rather than forcing the value.
  • Where does `orNull` pay off most?
    In generators for data classes with several optional fields. Each optional field lifted with `orNull` multiplies the null/non-null combinations explored, so the property covers absence patterns that nobody would write fixtures for by hand — which is exactly where the cheap, embarrassing null-handling bugs live.

saying these in an interview costs you the question

  • Thinking `orNull` replaces all values with null rather than mixing null in at a probability
  • Using `!!` or an early return to skip the null branch inside the property
  • Assuming a low null probability guarantees null is tested in every run
  • Adding `orNull` for a field the contract says can never be null
  • Believing the null rate is fixed and cannot be tuned

context