skip to content

Kotest's core artifacts and its integration modules for Spring, Testcontainers, WireMock and Koin are published as two different families. Why does that split matter in practice when you maintain a real test suite?

level: seniorimportance: nice to knowfreq 22%

answer

  1. io.kotest core vs io.kotest.extensions integrations
  2. separate release trains → versions do not transfer
  3. kotest-bom aligns core artifacts only
  4. extension = lifecycle management, else plain setup
  5. assertions usable without the Kotest runner

basics

~20 s

The integration modules live in a separate project under the io.kotest.extensions group with its own release train, so their versions are not the core version. You align core artifacts centrally, pin extension versions explicitly, and expect them to lag or lead core releases.

solid answer

~50 s

Two consequences matter day to day. **Versioning.** The core artifacts (`kotest-runner-junit5`, `kotest-assertions-core`, `kotest-property`, `kotest-framework-datatest`) release together and can be aligned via Kotest's BOM. The integrations under the `io.kotest.extensions` group are a separate project on its own release cadence, so their version numbers are unrelated to the core version. Copying the core version onto an extension dependency is the classic mistake; extension versions are pinned deliberately, and an extension may lag a major core release for a while. **Coupling and risk.** Every extension pulls the integrated library into your test classpath and binds you to two upgrade schedules at once — the third-party library's and the extension's. So adopt an extension when it manages a real lifecycle you would otherwise hand-roll badly (container start/stop, mock-server reset, framework test context). For a single spec that needs a trivial fixture, plain setup code has no version surface at all. The corollary is the good news: assertions are runner-independent, so you can adopt Kotest matchers without adopting anything else.

go deeper

for a junior

Know that the integrations are separate artifacts under a different group and have their own version numbers.

for a middle

Explain the independent release train, the BOM covering only core artifacts, and that assertions work without the runner.

for a senior

Turn it into maintenance judgement: pin extension versions explicitly, check extension availability before a major core upgrade, and adopt extensions only for real lifecycle management.

for a principal

Manage the test-classpath dependency surface deliberately — minimal modules, centrally aligned core versions, an explicit policy on which integrations are worth their coupling.

## Two families, two release trains Kotest publishes core functionality under the `io.kotest` group — the runner, the assertion library, property testing, data-driven testing — and its integrations under `io.kotest.extensions`, from a separate repository with its own releases. The extension family is where the Spring extension, the Testcontainers extension, the WireMock extension, the Koin extension, the Ktor assertions and the Arrow assertions live. That is an ordinary and sensible arrangement: an integration's release cadence is driven partly by the library it integrates with, which has nothing to do with when the core framework ships. But it has practical consequences a maintainer feels. ## Consequence 1: version numbers do not transfer The single most common setup mistake is assuming one version number spans everything. It does not. The core artifacts move together and can be aligned centrally — Kotest publishes a BOM (`kotest-bom`) for exactly that — but an extension's version is its own. Reusing the core version for an extension usually produces a "not found" resolution error, and occasionally something worse: a stale extension that resolves but was built against an older core. Practically: pin extension versions explicitly and separately, and treat a core upgrade as a moment to check whether the extensions you use have a matching release. Sometimes they will lag a major core version by weeks or longer, which is a real input into when you upgrade. ## Consequence 2: each extension is a coupling Adding an integration module couples the suite to three things at once: 1. the third-party library itself (Spring, Testcontainers, WireMock, Koin); 2. the extension's own release cadence; 3. the core Kotest version the extension supports. When those three fall out of alignment, upgrading any one of them can stall. So the adoption test is worth stating plainly: **an extension earns its place when it manages a lifecycle you would otherwise hand-roll**. Starting and stopping a container, guaranteeing teardown on the failure path, resetting a mock server between tests, or wiring a spec into a framework's test-context machinery are all things that are tedious and easy to get wrong by hand — and where the failure mode (leaked containers, state bleeding between specs) is expensive. By contrast, a fixture that is a couple of lines of setup does not need an integration module. The version surface you take on is not free. ## Consequence 3: the core is more decoupled than people assume The flip side is genuinely useful. `kotest-assertions-core` does not depend on the Kotest runner — its matchers raise ordinary assertion errors, so they work in tests executed by any JVM test framework. `kotest-property` is likewise usable outside Kotest specs. That makes incremental adoption cheap: bring in the matcher module purely for readability, leave the runner and spec styles for a later, separate decision, and abandon it just as cheaply if the team dislikes it. The same decoupling means an existing Kotest suite can keep using another assertion library where it already has investment; nothing forces uniformity at the assertion layer. ## Consequence 4: only add the modules you use JSON assertions, property testing and data-driven testing are separate artifacts on purpose. A suite that does not do property testing should not carry the property module. This keeps the test classpath small, which matters for resolution time and for the size of the dependency surface a security scan has to reason about. ## Consequence 5: know which family owns a problem When something breaks, the family tells you where to look. A spec that does not run at all points at the runner artifact. An unresolved `withData` or `Arb` points at a missing core module. A container that will not start, or an extension that no longer compiles after a Kotest upgrade, points at the extensions family and its independent versioning. That triage instinct is most of the practical value of knowing the split exists. ## What a strong answer includes Name the two groups, state that the extension family releases independently so version numbers do not transfer, mention the BOM for aligning core artifacts, and give the adoption test for extensions (lifecycle management versus a couple of lines of setup). Adding the runner-independence of the assertion modules — and what it enables for incremental adoption — is what turns a factual answer into a maintenance-judgement one.

  • You are upgrading to a new major Kotest core version and one extension has no matching release yet. What do you do?
    Treat the extension as a gating dependency: either delay the core upgrade, or temporarily replace what the extension does with explicit setup and teardown in the affected specs if the lifecycle is simple enough to hand-roll safely. Because the extension family releases independently, this is a foreseeable situation, so I'd check extension availability before scheduling a major core upgrade rather than discovering it mid-migration.
  • When is an integration extension not worth adding?
    When it wraps something you can express in a few lines of ordinary setup with no resource lifetime to manage. Every extension adds a third-party library to the test classpath plus two upgrade schedules to track, so the benefit has to be more than syntactic. The clear wins are resources that must be started and reliably stopped, or framework machinery that is genuinely intricate to wire by hand.

saying these in an interview costs you the question

  • Reusing the core Kotest version number for an integration extension dependency
  • Assuming the Kotest BOM aligns the independently released extension artifacts too
  • Adding an extension for a fixture that is two lines of plain setup
  • Believing the assertion library cannot be used without the Kotest runner
  • Treating a broken extension after a core upgrade as a core bug rather than a version-alignment issue

context