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.
answer
- concurrentSpecs / concurrentTests = coroutines
- parallelism = threads; dispatcherAffinity pins a spec to one thread
- @Isolate = spec runs alone
- blockingTest = true gives a blocking body its own thread
- breakage = shared db, singletons, sysprops, ports, static mocks
basics
~30 sconcurrentSpecs 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 sKotest 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 linesobject 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
Know that Kotest runs serially by default and that concurrency is opt-in through project configuration.
Distinguish concurrentSpecs from concurrentTests, and coroutine concurrency from thread parallelism; know @Isolate exists.
Own the rollout: enable per module, run repeatedly, isolate offenders, fix shared state, and use blockingTest for blocking bodies.
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