skip to content

In Kotest's property module, what does an `Exhaustive` generator such as `Exhaustive.enum<T>()` or `Exhaustive.of(...)` guarantee about the values a test sees, and what happens when the iteration count is larger or smaller than the domain?

level: middleimportance: should knowfreq 34%

answer

  1. Exhaustive = complete finite domain, walked in order
  2. iterations >= size → all covered, then cycles
  3. iterations < size → tail never tested, silently
  4. Exhaustive.enum<T>() survives new constants
  5. deterministic, nothing to shrink

basics

~20 s

An Exhaustive holds a finite list of values and the run walks it in order, cycling when there are more iterations than values. Every value is covered only if iterations are at least the domain size; fewer iterations means some values are never tested.

solid answer

~50 s

`Exhaustive<A>` is Kotest's generator for **small, finite, fully enumerable** domains — enums, booleans, a handful of known inputs, a small integer range. Unlike `Arb`, it is not random: it holds the complete list of values and the property run walks it in order. Two consequences follow directly from the domain size: - **iterations >= domain size**: every value is exercised, and values simply repeat (cycle) for the remaining iterations. This is the case you want — it makes the property a genuine claim over the whole domain. - **iterations < domain size**: values are consumed in order and the tail of the domain is never tested. The test still passes, silently under-covering. So if you lower iteration counts for speed, check your exhaustives. Because it is deterministic there is nothing to shrink — the domain is already minimal — and failures reproduce without depending on a seed. `Exhaustive.enum<Status>()` is the canonical use: add a new enum constant and the property covers it automatically.

code

kotlin · 3 lines
kotlin
checkAll(Exhaustive.enum<Currency>(), Arb.long(0..1_000_000)) { currency, minorUnits ->
    format(currency, minorUnits) shouldContain currency.symbol
}

go deeper

for a junior

Know that Exhaustive walks a fixed finite list of values, and that Exhaustive.enum<T>() covers every constant of an enum.

for a middle

Explain both directions of the iteration-versus-domain-size relationship, especially the silent under-coverage when iterations are too few, and give the enum use case.

for a senior

Add operational points: determinism means no seed dependence and nothing to shrink, mixing exhaustive categorical dimensions with random ones, and watching iteration counts as domains grow.

for a principal

Frame it as coverage design — which dimensions of the input model deserve enumeration versus sampling, and how the suite protects that guarantee as enums and runtime budgets change.

## What `Exhaustive` is Kotest's property module has two generator families under the common `Gen` type. `Arb` samples randomly from a large or infinite domain and mixes in edge cases. `Exhaustive` is the opposite: it carries the **complete, finite list of values** in the domain and hands them out in order. Typical constructors: ```kotlin Exhaustive.enum<PaymentStatus>() // every constant of the enum Exhaustive.of("GET", "POST", "PUT") // exactly these values listOf(1, 2, 3, 5, 8).exhaustive() // a collection promoted to a domain (1..12).exhaustive() // a small integer range ``` The guarantee is coverage, not randomness: given enough iterations, *every* value in the domain is used. ## Iterations versus domain size This is where the mechanics matter, and it is the part interviewers probe. **More iterations than values.** The run keeps pulling from the domain, cycling back to the start when it runs out. Every value is covered, several times over. The extra iterations add nothing for that dimension — an exhaustive dimension has no more information to give once it has been walked — but they are not harmful, and they matter if another generator in the same call is an `Arb` that keeps producing fresh values. **Fewer iterations than values.** The run consumes values in order and stops when the iteration budget runs out. The tail of the domain is **never tested**, and nothing warns you. This is the real trap: someone drops the iteration count to speed the suite up, or writes `checkAll(10, someBigExhaustive)`, and a supposedly exhaustive property quietly becomes a partial one. If you rely on an `Exhaustive` for coverage, make sure the iteration count is at least the domain size — and be conscious of it when tuning suite runtime. ## When to reach for it - **Enums.** `Exhaustive.enum<T>()` is the single best reason to know this type: it makes the property a statement about *all* constants, and it keeps working when someone adds a constant next quarter. That is coverage that no hand-written example test list gives you. - **Small closed sets** — HTTP verbs, supported currencies, feature-flag combinations, `Boolean`. - **Small integer ranges** where every value genuinely matters (months, retry counts, day-of-week). And when *not* to: anything where the domain is large. An exhaustive over thousands of values is either a very slow property or, worse, a silently under-covering one. That is `Arb` territory. ## Mixing with `Arb` Because both are `Gen`, a multi-generator property can take one of each — the everyday pattern being "enumerate the categorical dimension, randomise the open-ended one": ```kotlin checkAll(Exhaustive.enum<Currency>(), Arb.long(0..1_000_000)) { currency, minorUnits -> format(currency, minorUnits) shouldContain currency.symbol } ``` Here the iteration count should comfortably exceed the number of currencies so every currency is seen many times, each with different random amounts. ## Determinism, shrinking and reproducibility An `Exhaustive` run is deterministic in its values: the same domain in the same order every time. That has two pleasant consequences. First, failures reproduce without depending on the run's seed — the failing value was going to be tested anyway. Second, shrinking has nothing to do: the framework's job in shrinking is to find a smaller failing input, and a domain of enum constants or three HTTP verbs has no meaningful smaller form. So when a property fails on an exhaustive dimension, the reported value *is* the counter-example, full stop. ## Practical guidance - Prefer `Exhaustive.enum<T>()` over hand-listing constants; the point is that it survives the enum changing. - Keep exhaustive domains genuinely small; if you find yourself enumerating hundreds of values, use an `Arb` with the right constraints instead. - Check the iteration count whenever an exhaustive domain grows — a new enum constant plus a low iteration count is how coverage silently regresses. - Use `Exhaustive.of(...)` for ad-hoc closed sets that are not an enum in the code (protocol strings, supported locales), and consider promoting them to a real enum if the test keeps needing them. - Remember the coverage claim is per-dimension: enumerating one parameter does not enumerate the *combinations* with another generator's values, so if a specific combination matters, assert it in an example test as well.

  • Why is `Exhaustive.enum<T>()` better than listing the constants in an `Exhaustive.of(...)` call?
    Because it tracks the enum. When someone adds a constant, the property automatically covers it, whereas a hand-written list silently keeps testing the old set. That auto-updating coverage is the main reason enums are the flagship use of `Exhaustive`, and it is why reviewers should flag hand-listed enum constants in property tests.
  • A property uses an `Exhaustive` domain of 50 values but runs 10 iterations. What happens?
    The first ten values are used and the remaining forty are never tested; the property passes on partial coverage with no warning. The fix is to ensure the iteration count is at least the domain size, and to re-check that whenever the domain grows or someone lowers iteration counts to speed the suite up.
  • Does shrinking apply to a failure on an `Exhaustive` dimension?
    There is nothing meaningful to shrink: the domain is a fixed finite list, so the failing value is already the counter-example and there is no smaller form to search for. This also means such failures reproduce deterministically without relying on the run's seed, unlike a failure driven by an `Arb`.

saying these in an interview costs you the question

  • Believing `Exhaustive` guarantees full coverage regardless of the iteration count
  • Using `Exhaustive` for a large domain because "exhaustive sounds more thorough"
  • Hand-listing enum constants instead of `Exhaustive.enum<T>()`
  • Thinking `Exhaustive` values are randomised or shuffled per run
  • Assuming that enumerating one parameter also enumerates its combinations with other generators

context