skip to content

Ordering & Concurrency

Kotest can randomize or fix test and spec ordering and run specs or invocations in parallel. You should know which knobs exist and how ordering plus concurrency interacts with isolation mode and shared state — a favorite follow-up on flaky-test questions.

on this pageshow

explore

questions

5

You want a large Kotest suite to run faster by executing work in parallel. Explain the knobs Kotest gives you — concurrentSpecs, concurrentTests, parallelism, the @Isolate annotation and the blockingTest test-config flag — and what breaks when you turn them on.

level: seniorimportance: must knowfreq 35%

answer

  1. concurrentSpecs / concurrentTests = coroutines
  2. parallelism = threads; dispatcherAffinity pins a spec to one thread
  3. @Isolate = spec runs alone
  4. blockingTest = true gives a blocking body its own thread
  5. breakage = shared db, singletons, sysprops, ports, static mocks

basics

~30 s

concurrentSpecs sets how many spec classes run concurrently and concurrentTests how many tests inside a spec do; both are coroutine concurrency. parallelism sets the number of threads the engine dispatches specs onto. @Isolate marks a spec that must never run alongside others, and config(blockingTest = true) gives a blocking test its own thread so it cannot starve the shared dispatcher. What breaks is anything global: shared databases, singletons, system properties, static mocks, fixed ports.

solid answer

~60 s

Kotest separates two things people conflate. **Concurrency (coroutines)**: `concurrentSpecs` in `AbstractProjectConfig` sets how many specs may be in flight at once, and `concurrentTests` how many tests within a spec may be. Both default to one at a time. These launch coroutines, so they overlap suspending work well. **Parallelism (threads)**: `parallelism` sets how many threads the engine uses to dispatch specs. Related is `dispatcherAffinity`, which by default keeps all of a spec's tests on the same thread — important when code under test is thread-confined. **Escape hatches**: annotate a spec `@Isolate` and Kotest runs it alone, never concurrently with others — the marker for specs that mutate global state. Mark a test `.config(blockingTest = true)` when its body blocks a thread rather than suspending, so it does not starve the shared dispatcher. What breaks is anything shared: one test database, singletons and companion-object state, system properties, static mocks, fixed ports, temp files with fixed names. Turn concurrency on per module, isolate the offenders, and fix the sharing rather than serialising everything.

code

kotlin · 13 lines
kotlin
object ProjectConfig : AbstractProjectConfig() {
   override val concurrentSpecs = 4
   override val parallelism = 4
}

@Isolate
class SystemPropertySpec : FunSpec({
   test("mutates a process-wide property") { }
})

class LegacyClientSpec : FunSpec({
   test("calls a blocking driver").config(blockingTest = true) { }
})

go deeper

for a junior

Know that Kotest runs serially by default and that concurrency is opt-in through project configuration.

for a middle

Distinguish concurrentSpecs from concurrentTests, and coroutine concurrency from thread parallelism; know @Isolate exists.

for a senior

Own the rollout: enable per module, run repeatedly, isolate offenders, fix shared state, and use blockingTest for blocking bodies.

for a principal

Decide how much concurrency the suite should carry against infrastructure cost and debuggability, and set the standard that @Isolate is tracked debt rather than a solution.

## Two axes: coroutines and threads Kotest test bodies are suspending functions, so the framework can interleave many of them on few threads. That gives two distinct knobs. **Concurrency** is how many test coroutines may be in flight at once, set through `AbstractProjectConfig`: - `concurrentSpecs` — how many spec classes run concurrently. - `concurrentTests` — how many tests inside a single spec run concurrently. Both are one-at-a-time by default, which is why a fresh Kotest suite is fully serial. They compose: concurrent specs each running concurrent tests multiplies the in-flight count, so raise one at a time and measure. **Parallelism** is how many threads the engine dispatches onto, set by `parallelism` in the same config. Alongside it, `dispatcherAffinity` (on by default) keeps all tests of a given spec on the same thread. That default matters when the code under test is thread-confined — anything keyed off a thread-local, or context propagation that assumes one thread — because a spec's tests then see a consistent thread even under load. The distinction is worth stating explicitly: raising concurrency without threads still speeds up IO-bound suites, because a suspended test frees its thread; raising threads helps CPU-bound test bodies. Most integration suites are IO-bound and benefit most from concurrency. ## The escape hatches Real suites always contain specs that cannot share the machine. `@Isolate` on a spec class tells Kotest to run that spec alone, never concurrently with others. Use it for specs that mutate process-global state — system properties, static mocks, a shared singleton, a fixed port, a frozen clock. It is the pressure valve that lets you enable concurrency globally without rewriting the whole suite on day one, but each `@Isolate` is debt: it names a spec whose isolation is broken. `.config(blockingTest = true)` addresses the other failure mode. A test body that *blocks* its thread — a sleep, a blocking JDBC call, a legacy client — does not release the thread the way a suspending call does, so under concurrency it can starve other tests sharing that dispatcher. Marking the test as blocking gets it its own thread so the rest keeps moving. ## What actually breaks The failures are all forms of sharing: - **One database.** Two tests inserting the same key, or one truncating tables while another reads. Fixes: per-test transaction rollback, unique keys per test, or a schema or container per concurrent worker. - **Singletons and companion objects.** Any mutable global becomes a race. - **System properties and environment tricks.** Process-wide by definition — `@Isolate` territory. - **Static mocks and global stubs.** Configured by one test, seen by another. - **Fixed ports and fixed temp-file names.** Two specs binding the same port fail nondeterministically. - **Order-dependent tests.** Concurrency destroys the implicit ordering they relied on, so it surfaces the same defects random ordering does, more violently. ## Rollout that works Start in one module. Turn on `concurrentSpecs` first, since cross-spec isolation is usually better than within-spec isolation. Run the suite repeatedly — concurrency bugs are probabilistic, so a single green run proves nothing. `@Isolate` the specs that fail, file the isolation debt, and only then consider `concurrentTests`. Measure wall-clock at each step: if the suite is dominated by one slow spec, concurrency across specs buys nothing until that spec is split. Finally, expect reporting to change: interleaved output makes logs harder to read, and a failure's context can be mixed with unrelated work. Structured per-test output capture matters more once concurrency is on.

  • What is the difference between raising concurrentSpecs and raising parallelism?
    concurrentSpecs controls how many spec coroutines are in flight; parallelism controls how many threads the engine dispatches them onto. An IO-bound suite gains from concurrency alone, because a suspended test releases its thread for another. A CPU-bound suite needs threads, since the work never yields. They are usually raised together, but knowing which bottleneck you have tells you which one is actually buying the speedup.
  • A spec passes alone and fails once concurrency is enabled. What are your first checks?
    Look for state outside the test: a shared database without per-test rollback, a mutable singleton or companion object, a system property, a static mock, a fixed port or temp-file name, or a frozen clock. Reproduce by running the suite several times rather than once, since the failure is probabilistic. Mark the spec @Isolate to unblock the build, then fix the sharing so the annotation can be removed rather than left as permanent scaffolding.

saying these in an interview costs you the question

  • Treating concurrentSpecs and parallelism as the same knob
  • Assuming a single green run proves a suite is concurrency-safe
  • Leaving @Isolate on specs permanently instead of fixing the shared state
  • Enabling concurrency while tests share one database with no per-test isolation
  • Expecting a blocking test body to yield its thread the way a suspending call does

context

open as a page

Kotest's TestCaseOrder has the values Sequential, Random and Lexicographic. What does each do, what is the default, where do you configure it, and why would a team deliberately run tests in random order?

level: middleimportance: should knowfreq 35%

basics

~20 s

TestCaseOrder controls the order of sibling tests within a spec: Sequential is declaration order and is the default, Lexicographic sorts by name, Random shuffles. Set it per spec by overriding testCaseOrder(), or globally in the project config. Random is used to expose tests that secretly depend on each other's leftover state.

open as a page

You inherit a Kotest suite of several thousand tests that takes 40 minutes and runs entirely serially. How would you decide how much of it to run concurrently, and how would you keep the result trustworthy rather than intermittently red?

level: principalimportance: should knowfreq 25%

basics

~20 s

Measure first to find whether the time is IO-bound or dominated by a few slow specs, then raise Kotest's concurrentSpecs before concurrentTests, module by module. Run each step many times, @Isolate the specs that fail while tracking them as debt, fix shared state rather than serialising around it, and pair the rollout with randomised ordering so latent coupling surfaces deliberately.

open as a page

Kotest's test config accepts an invocations parameter. What does it do, which kinds of test can use it, and when is repeating a test body the wrong tool for the job?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

invocations runs a leaf test's body the given number of times; a failure in any repetition fails that one test case. It cannot be used on containers. It is the wrong tool for exploring input space — that is property testing — and a poor substitute for fixing a flaky test.

open as a page

Kotest's SpecExecutionOrder controls the order whole spec classes run in, with values including Undefined, Lexicographic, Random, Annotated and FailureFirst. Walk through what each means, how the @Order annotation participates, and what FailureFirst needs in order to work.

level: middleimportance: nice to knowfreq 25%

basics

~20 s

SpecExecutionOrder orders spec classes, not tests inside them. Undefined is the default and means discovery order; Lexicographic sorts by class name; Random shuffles; Annotated honours the @Order annotation, running annotated specs first in ascending order with unannotated ones after; FailureFirst runs previously failed specs first, which requires Kotest's persisted record of the last run's failures on disk.

open as a page