A Kotest property test failed once in CI and passes on your machine. The failure output includes a seed value. How do you use it to reproduce the failure, and what does it not guarantee?
answer
- failure output prints the run's seed
- PropTestConfig(seed = n) replays the draw sequence
- same generators, same order, same iteration count
- impurity (Random, clock, DB) is not replayed
- keep the counterexample as an example test; unpin the seed
basics
~20 sKotest prints the seed that drove the run's random source. Re-run the property with PropTestConfig(seed = thatValue) and the same generators produce the same samples in the same order, reproducing the failure — unless the test depends on anything outside that random source.
solid answer
~50 skotest-property drives generation from a seeded random source, and on failure the report includes the seed so you can replay the run: ```kotlin checkAll(PropTestConfig(seed = 4_872_003_991L), Arb.int(), Arb.string()) { i, s -> ... } ``` With the same seed, the same generators, in the same order, with the same iteration count, Kotest produces the same sequence of samples — so the failing iteration comes back. What it does **not** guarantee: - **Changed generators or argument order** re-shuffle the draws; the seed is not a recording of values. - **Anything outside the random source** — `kotlin.random.Random` inside a generator or test body, the clock, environment, database state, a shared mutable fixture — is not replayed. - **Concurrency and ordering effects** in the code under test are not pinned by the seed. Workflow: replay with the seed, confirm the failure, read the shrunk counterexample, then write that counterexample as a plain example-based test so the regression is pinned independently of the seed.
code
kotlin · 4 lines// temporary: reproduce the CI failure locally
checkAll(PropTestConfig(seed = 4872003991L), Arb.int(), Arb.string()) { i, s ->
roundTrip(i, s) shouldBe (i to s)
}go deeper
Know that Kotest prints a seed on failure and that passing it via PropTestConfig(seed = …) re-runs the same generated inputs.
Give the full workflow — replay, fix, convert the counterexample into an example test, unpin — and name what the seed cannot reproduce.
Lead with the purity requirement: the seed is only as good as the determinism of the generators and the code under test; treat non-reproduction as an impurity hunt.
Make it policy: pure generators, no committed seeds, counterexamples promoted to regression tests, and seeds plus counterexamples both captured in the incident record.
## Why a seed exists Property tests generate different inputs every run, which is exactly what makes them valuable and exactly what makes a failure frustrating: the CI job that just went red may not go red again. kotest-property solves this by driving all generation from a single seeded random source. Every draw the run makes — for every argument, in order, across every iteration — is determined by that seed. When a property fails, the reported output includes the seed for that run so the exact sequence can be recreated. ## The replay workflow 1. **Copy the seed from the failure output.** 2. **Pin it temporarily** on the failing property: ```kotlin checkAll(PropTestConfig(seed = 4872003991L), Arb.int(), Arb.string()) { i, s -> encode(i, s) shouldBe expected(i, s) } ``` 3. **Run locally and confirm** the same failure appears, with the same shrunk counterexample. 4. **Fix the code**, verify with the seed pinned. 5. **Turn the counterexample into a regression test.** Take the minimal input the shrinker reported and write it as an ordinary example-based test — a `shouldBe` on the concrete values. That test does not depend on generator internals or seed arithmetic and will still guard the bug in two years. 6. **Remove the pinned seed** so the property goes back to exploring. ## The guarantees and their limits The seed determines the *random source*, not the *values*. Everything that maps that source onto concrete samples must be unchanged for replay to work: - **Same generators.** Widening `Arb.int()` to `Arb.int(0..100)`, adding an edge case, changing a string's size bounds — any of these changes what a given random draw becomes. - **Same argument list and order.** Adding a third generator, or swapping two, re-partitions the draws across arguments. - **Same iteration count.** Fewer iterations may never reach the failing one. - **Same Kotest version, broadly.** Generator implementations can change between versions; a seed is not a portable artefact across upgrades. And critically, the seed cannot reproduce anything that never came from the random source: - `kotlin.random.Random`, `Instant.now()`, `UUID.randomUUID()` called inside a generator or a test body. - Data read from a database, file, environment variable or system property. - Mutable state left behind by a previous test (a shared object, a cache, a static counter). - Non-determinism in the code under test: thread scheduling, iteration order of a hash-based collection over identity hashes, timeouts. When the seed does not reproduce a failure, that list is your checklist — and the most common culprit by far is impurity in the generator or the test body. ## Practical habits - **Keep generators and property bodies pure** so the seed is actually meaningful. This is the single highest-value rule around seeds. - **Don't leave the seed pinned in the committed test.** A pinned seed makes the test deterministic — and permanently blind, since it will only ever explore that one sample sequence. - **Record the seed and the shrunk counterexample in the bug report**, not just the seed: the counterexample is the durable artefact, the seed is the reproduction convenience. - **If a failure is rare and you cannot pin it**, raising the iteration count for a local investigation run is a reasonable way to hunt for another instance of the same class of failure. ## Interview signal This is a practical, everyday question: interviewers want to hear the workflow (copy seed → pin via `PropTestConfig(seed = …)` → reproduce → fix → convert the counterexample to an example test → unpin), plus an honest account of what the seed cannot pin. A candidate who says "the seed guarantees reproducibility" without qualification has not debugged an impure generator yet.
- You pin the reported seed and the test still passes. What are the candidate explanations?Something outside Kotest's random source is feeding the test: kotlin.random.Random or a clock inside a generator or body, external data such as a database row or environment variable, state left by another test, or non-determinism in the code under test such as thread scheduling. It can also mean the generators, their order, or the iteration count changed since the failing run, since the seed pins the random stream rather than the values.
- Why should you not commit the pinned seed once the bug is fixed?A committed seed freezes the property onto one sample sequence, so it stops exploring and loses the main benefit of property testing — finding inputs you did not think of. The durable protection is the shrunk counterexample written as an ordinary example-based test, which pins the specific regression without constraining the property's future coverage.
saying these in an interview costs you the question
- Claiming the seed guarantees reproduction regardless of what the test does
- Committing PropTestConfig(seed = …) permanently as the way to make property tests "stable"
- Not realising that changing the generators or their order invalidates an old seed
- Reporting only the seed in a bug ticket and losing the shrunk counterexample
- Thinking the seed records the generated values rather than driving the random source