Kotest
Kotest is the Kotlin-native test framework combining ten spec styles, a fluent matcher DSL, property-based testing, and rich lifecycle configuration on top of the JUnit Platform. Interviewers use it to probe whether you know idiomatic Kotlin testing beyond JUnit habits and can configure a real test suite, not just write assertions.
on this pageshowhide
explore
- Spec Styles16 questions
- Flat Styles4 questions
- Nested & Structured Styles4 questions
- BDD Styles4 questions
- Choosing a Style & Naming4 questions
- Matchers & Assertions19 questions
- Equality & Type Matchers5 questions
- Collection & String Matchers4 questions
- Exceptions, Soft Assertions & Clues5 questions
- Custom Matcher Composition5 questions
- Property-Based Testing20 questions
- checkAll & forAll4 questions
- Arb & Exhaustive Generators5 questions
- Composing Generators (Arb.bind)6 questions
- Shrinking, Edge Cases & Seeds5 questions
- Data-Driven Testing9 questions
- withData & Test Name Generation5 questions
- Tables: row & forAll4 questions
- Lifecycle & Configuration29 questions
- Lifecycle Hooks4 questions
- Isolation Modes5 questions
- Listeners & Extensions6 questions
- Project-Level Config5 questions
- Tags & Conditional Execution4 questions
- Ordering & Concurrency5 questions
- Platform & Ecosystem Integrations15 questions
- JUnit Platform & Gradle Setup4 questions
- Non-Deterministic Testing (eventually)5 questions
- Coroutine Support3 questions
- Assertion & Extension Modules3 questions
questions
108 · 6 sectionsIn Kotest's nested spec styles, what exactly happens when the body of a container block — a `describe`, a `context`, or a FreeSpec `-` group — runs, and why is putting setup code or assertions directly in that body a common source of bugs?
basics
~20 sA container is itself a test node whose body executes during the run to register and run its children — it is not a before-hook. Code in it runs inline, may re-execute per leaf depending on the isolation mode, and a throw there fails the container and prevents its unregistered children from ever running.
How do you temporarily disable a whole Given block or a single Scenario in a Kotest BDD spec, and what happens to the blocks nested inside it?
basics
~20 sPrefix the block with x: xgiven, xwhen, xthen, xand, xfeature, xscenario. Kotest marks that node disabled and never executes its body, so nested blocks are never registered and never appear individually - only the disabled node is reported as ignored.
Kotest test names are free-text strings rather than method identifiers. How are those names supplied, and where do they end up mattering?
basics
~20 sThe name is the string argument you pass to the DSL builder, evaluated when the spec registers its tests. It becomes the test's identity: shown in IDE and CI reports, used for name-based filtering, and it must be unique within its scope.
Kotest's ShouldSpec registers tests with `should("...") { }`. What does that call actually create, and how do you group several such tests inside one ShouldSpec?
basics
~20 sshould("...") { } registers one leaf test whose reported name reads as "should ...". Grouping is done with context("...") { } containers, which may hold should leaves and further nested contexts. should also works at the spec root.
Kotest's WordSpec builds tests out of `should` and `When` blocks. What does a WordSpec declaration look like, how are the reported names assembled from it, and how deeply can it nest?
basics
~20 sYou write "a stack" should { "pop returns the last item" { ... } }. The should block is a container whose name renders as "a stack should"; the inner strings are leaf tests. An optional outer When block adds one scenario layer. Nesting is fixed: When → should → test.
In Kotest, what exactly is the difference between shouldContainExactly, shouldContainExactlyInAnyOrder and shouldContainAll — particularly with respect to ordering, size and duplicate elements?
basics
~20 sshouldContainExactly requires the same elements in the same order and the same size. shouldContainExactlyInAnyOrder requires the same multiset — same size and same duplicate counts, order irrelevant. shouldContainAll is only a subset check: extras and duplicate mismatches are ignored.
In Kotest, what equality does the shouldBe matcher actually apply — how does it compare arrays, data classes and collections, and where does that differ from a plain Kotlin == check?
basics
~20 sKotest's shouldBe does not simply call equals. It dispatches on runtime type first: arrays are compared by contents rather than by reference, iterables and maps element-wise with a positional diff, data classes with per-field failure reporting. Everything else falls back to equals.
Kotest's MatcherResult carries a passed flag plus two messages — failureMessage and negatedFailureMessage. Under what circumstances does each message reach the developer, and what breaks if you write them carelessly?
basics
~20 sfailureMessage is shown when the matcher is used positively (should / a shouldX extension) and does not pass. negatedFailureMessage is shown when it is used negatively (shouldNot) and it does pass. Copying the positive text into the negated slot makes failures report the opposite of reality.
Kotest's shouldThrow<T> block returns a value. What is that value, how do you assert on it, and when do you need the shouldThrowUnit variant instead?
basics
~20 sIt returns the caught exception, already typed as T. Bind it and assert on message, cause or custom fields. Use shouldThrowUnit<T> when the block's last expression is Unit, because plain shouldThrow expects a block returning Any?.
How does Kotest's assertSoftly actually aggregate failures — what does it collect, what does it report, and which failures inside the block does it NOT aggregate?
basics
~20 sInside the block Kotest switches its error collector to soft mode, so matcher failures are recorded instead of thrown; at the end it throws one MultiAssertionError listing them all. Only failures routed through Kotest's collector are aggregated — non-assertion exceptions abort the block immediately.
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?
basics
~20 sPass 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.
In Kotest's property-testing module, what is the difference in contract between `checkAll`, `forAll` and `forNone`, and how does each one decide that a property has failed?
basics
~20 scheckAll's lambda returns Unit and fails when an assertion inside it throws. forAll's lambda must return Boolean and fails when it returns false. forNone is the inverse — it fails when the predicate returns true.
Kotest's `Arb` is usually described as producing random values, yet property runs frequently hit values like 0, `Int.MIN_VALUE`, empty strings and empty lists. What is actually in an Arb's output stream?
basics
~20 sAn Arb is not purely random: it declares a set of edge cases alongside its random sampler, and Kotest interleaves those edge cases into the value stream with a small, configurable probability, so boundary values appear far more often than chance would produce.
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?
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.
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.
basics
~20 smap 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.
When a Kotest table-driven check (`forAll` over a `table(headers(...), row(...), ...)`) fails on some rows, do the remaining rows still run, and what does the resulting failure message contain?
basics
~20 sEvery row runs. Kotest collects the failures instead of stopping at the first one and then throws a single error that lists each failing row with its header/value pairs and that row's assertion error, so you see all bad rows in one run.
When you drive tests from a collection of values with Kotest's `withData`, how does the framework derive the name of each generated test, and what happens when the elements are not data classes?
basics
~20 sKotest asks the element for a stable identifier: a WithDataTestName implementation wins, then an @IsStableType-annotated class's toString, then a data class's toString. Anything else falls back to a type-derived name, so every case ends up named the same.
How is Kotest's table DSL typed — what do `headers(...)`, `row(...)` and `table(...)` produce, how does the `forAll` lambda receive the columns, and what are the limits of that design?
basics
~20 sEach arity has its own generated type — row(1, "a") is a two-column row, and table(headers, rows...) fixes both the column count and each column's type. forAll then hands the lambda one typed parameter per column. Limits: fixed arity, one type per column across all rows, positional access only.
Kotest's `withData` generates test names from the input values. What mechanisms does Kotest give you to override that generated name, and when would you pick each one?
basics
~20 sFour hooks: implement WithDataTestName on the type, annotate the class @IsStableType so its toString is trusted, pass the nameFn overload withData({ "case ${it.id}" }, values), or pass a Map<String, T> whose keys become the names verbatim.
Where can Kotest's `withData` be placed inside a spec — at the spec root, inside a container, or inside another `withData` — and what kind of node does each element become?
basics
~20 sIt can sit at the spec root, inside a container such as FunSpec's context, and inside another withData. Each element is registered as a node whose body is a container scope: it reports as a leaf test if nothing nested is registered, and as a container if the body registers more tests.
In a Kotest spec, how do you run setup code before every test and cleanup code after every test, and what do those callbacks receive?
basics
~20 sDeclare Kotest's beforeTest { } and afterTest { } blocks in the spec body. beforeTest receives the TestCase about to run; afterTest receives a TestCase/TestResult pair. Both are suspend lambdas, so suspending setup and cleanup work directly.
In Kotest 5, what are the ways to register a listener or extension, and how does the registration site change what it affects?
basics
~20 sInside a spec via extension(...) or override fun extensions() — that spec only. In a class extending Kotest's AbstractProjectConfig via override fun extensions() — every spec in the run. Or @AutoScan for classpath-discovered global extensions.
Kotest offers beforeTest, beforeEach, beforeContainer and beforeAny. In a nested spec where tests live inside a context block, which of these fires for which test cases?
basics
~10 sKotest treats containers and leaves both as test cases. beforeEach/afterEach fire only for leaf tests; beforeContainer/afterContainer only for containers; beforeTest/afterTest and their aliases beforeAny/afterAny fire for both.
Kotest's IsolationMode controls how many instances of a spec class are created for a run. Walk through what SingleInstance, InstancePerRoot, InstancePerTest and InstancePerLeaf each do for a spec with nested containers.
basics
~20 sSingleInstance: one instance for all tests. InstancePerRoot: a fresh instance per top-level test, everything nested under it shares that instance. InstancePerTest: a fresh instance per test case, containers included. InstancePerLeaf: a fresh instance per leaf test, with its container path re-executed.
How does Kotest 5 locate your AbstractProjectConfig class at runtime, and what would you check if its settings appear to be ignored?
basics
~20 sWrite one class (or object) extending AbstractProjectConfig with a no-arg constructor. Kotest 5 picks it up by convention from the io.kotest.provided package on the test classpath, or from the fully qualified name given in the kotest.framework.config.fqn system property. One config applies per run.
In Kotest, test bodies and lifecycle callbacks are declared as suspending functions. What does the framework do differently because of that, and what does it change about how you write a test that touches suspending code?
basics
~20 sKotest invokes every test body — and every lifecycle callback like beforeTest and beforeSpec — inside a coroutine, so suspending calls are made directly with no wrapper builder. The test scope is itself a CoroutineScope, and Kotest enforces the test's configured timeout around that coroutine.
A developer puts JUnit 5 annotations like @BeforeEach, @Disabled and @Tag inside a Kotest FunSpec and nothing happens — no setup runs, the "disabled" test still executes. Why are they inert, and what are the Kotest-side equivalents?
basics
~20 sKotest specs are executed by Kotest's own TestEngine, not JUnit Jupiter, and Jupiter annotations only mean something to the Jupiter engine. Use Kotest's own facilities instead: beforeTest/afterTest/beforeSpec, .config(enabled = false) or the x-prefixed builders, @Ignored on the spec, and Kotest Tag objects.
Kotest's assertion library ships `eventually`, `until` and `continually` for testing asynchronous behaviour. What does each one assert, and how do their pass and fail conditions differ?
basics
~20 seventually re-runs a block until it stops throwing, within a deadline — the first success passes. until does the same for a boolean predicate becoming true. continually requires the block to keep passing for the whole duration — the first failure fails the test.
What does the kotest-runner-junit5 artifact actually contribute at test time, and how does a class like FunSpec end up being executed and reported inside a JUnit Platform run?
basics
~20 sIt contains Kotest's JUnit Platform TestEngine implementation. The platform discovers that engine alongside any others, hands it the discovery request, and the engine finds Kotest spec classes, runs their tests with Kotest's own coroutine-based executor, and reports each test case back to the platform as it goes.
Kotest is published as several separate artifacts rather than one jar. Name the main ones and say what each provides — matchers, property testing, data-driven testing, JSON assertions, and the test runner.
basics
~10 skotest-runner-junit5 runs specs; kotest-assertions-core holds the shouldBe/shouldContain matcher family; kotest-property provides Arb/Exhaustive property testing; kotest-framework-datatest provides withData; kotest-assertions-json adds JSON matchers. You add only the modules you use.