skip to content

How do you configure iteration count, edge-case injection, seeds, and discards in Kotest property tests, and why do edge cases matter?

level: middleimportance: should knowfreq 35%

answer

  1. PropTestConfig: iterations, seed, edgeConfig, maxDiscardPercentage
  2. Default 1000 iterations
  3. seed printed on failure → reproduce
  4. Edge cases = 0, MIN/MAX, empty, blank, null
  5. Bound the Arb instead of filtering to avoid discards

basics

~20 s

You can set how many random inputs to try, control the share of special edge values, fix a seed so failures repeat, and limit how many inputs may be skipped. Edge cases catch boundary bugs like zero or empty.

solid answer

~30 s

Tuning happens via `PropTestConfig` passed to `checkAll`/`forAll`, or via inline params. Key knobs: `iterations` (default ~1000) controls how many samples run; `seed` fixes the RandomSource so a failure reproduces exactly; `edgeConfig`/`PropTestConfig(edgeConfig = EdgeConfig(edgecasesGenerationProbability = …))` controls how often injected edge cases appear; `maxDiscardPercentage` limits how many filtered/`Assumptions`-rejected samples are tolerated before failing with 'too many discards'. Edge cases matter because Arbs deterministically inject boundary values (0, MIN/MAX, empty, blank, null via `.orNull()`) where off-by-one and overflow bugs cluster — random sampling alone rarely hits exact boundaries. You can also constrain ranges (`Arb.int(0..100)`) instead of filtering to avoid discards.

code

kotlin · 16 lines
kotlin
import io.kotest.property.*
import io.kotest.property.arbitrary.int
import io.kotest.matchers.shouldBe

suspend fun configured() {
    checkAll(
        PropTestConfig(
            iterations = 3000,
            maxDiscardPercentage = 15,
            edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.25)
        ),
        Arb.int(0..10_000)
    ) { n ->
        (n + 0) shouldBe n
    }
}

go deeper

for a junior

Knows you can set iteration count and that a seed lets a failure repeat.

for a middle

Uses PropTestConfig fields (iterations, seed, maxDiscardPercentage) and explains why edge cases catch boundary bugs.

for a senior

Diagnoses 'too many discards' by narrowing generators, tunes edgecasesGenerationProbability, and sets project-wide defaults.

for a principal

Establishes suite-wide PropTestConfig policy (iteration budgets, seed logging for CI repro, discard limits) balancing speed and bug-finding power.

## Where configuration lives Property runners accept a **`PropTestConfig`** (or inline arguments) controlling the run: ```kotlin import io.kotest.property.* import io.kotest.property.arbitrary.int checkAll( PropTestConfig(iterations = 5000, seed = 42L), Arb.int() ) { n -> /* property */ } ``` You can also pass `checkAll(iterations = 5000, Arb.int()) { … }` directly. ## Iterations - **`iterations`** = number of samples to run. **Default is 1000** for a single generator; with multiple generators Kotest may compute a count based on the generators. More iterations = more confidence but slower. ## Seeds and reproducibility - **`seed`** fixes the `RandomSource`. On failure Kotest prints the seed it used; set that seed to deterministically reproduce the exact sequence of generated values. Essential for debugging CI-only failures. Don't pin a seed permanently in normal runs — that defeats randomized coverage. ## Edge-case injection Every `Arb` defines **edge cases**: deterministic boundary values yielded *before/among* random ones. Examples: - `Arb.int()` → `0`, `Int.MIN_VALUE`, `Int.MAX_VALUE` - `Arb.string()` → empty, blank - `Arb.list(...)` → empty list - `.orNull()` → `null` Why they matter: bugs cluster at **boundaries** (off-by-one, overflow, empty-collection NPEs). Pure random sampling almost never lands on `Int.MAX_VALUE` exactly, so without injection these bugs stay hidden. Control frequency via `EdgeConfig`: ```kotlin PropTestConfig( edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.3) ) ``` A probability of `0.0` effectively disables edge injection; higher values test boundaries more aggressively. ## Discards (assumptions / filtering) When you use `filter` on an Arb, or call **`Assumptions`** (e.g. an inline guard that returns early / `assume(cond)`), rejected samples are **discarded**. Too many discards means your generator wastes effort. Kotest caps this via: ```kotlin PropTestConfig(maxDiscardPercentage = 20) ``` Exceeding it throws a 'too many discards' error, signaling you should **narrow the generator** instead of filtering — e.g. `Arb.int(0..100)` rather than `Arb.int().filter { it in 0..100 }`. ## Putting it together ```kotlin checkAll( PropTestConfig( iterations = 2000, seed = null, // random; printed on failure maxDiscardPercentage = 10, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2) ), Arb.int(0..1000) ) { n -> (n * 2) shouldBe (n + n) } ``` ## Global defaults Project-wide defaults (iterations, etc.) can be set via Kotest's `AbstractProjectConfig` so you don't repeat config per test.

  • You get a 'too many discards' failure. What's the fix?
    Stop filtering; construct a generator that only produces valid values (e.g. Arb.int(0..100) or Arb.bind with constrained components) so few samples are rejected.
  • Why not hardcode a seed in every property test?
    A fixed seed makes the test deterministic but eliminates the randomized exploration that finds new bugs; pin a seed only to reproduce a specific failure.

saying these in an interview costs you the question

  • Solves slow/failing generators by raising maxDiscardPercentage instead of fixing the generator
  • Doesn't know edge cases are injected deterministically
  • Pins a seed permanently, killing coverage
  • Cannot name PropTestConfig or any of its fields

context