A module contains both Kotest specs and JUnit 5 Jupiter test classes running in the same test task. What do the two engines share and what do they definitely not share, and how would you decide whether to keep the mix?
answer
- shared: JVM, classpath, sysprops, one report
- not shared: lifecycle, extensions, config, tags, parallelism
- tag filtering must be expressed twice — silent under-testing
- fixtures as plain code, not engine extensions
- standardise per module; mix is a migration state
basics
~20 sThey share the JVM, classpath, system properties and the combined report — nothing more. Lifecycle, extensions, configuration, tag filtering, parallelism and timeouts are per-engine, so no setup crosses over. Mixing is fine as a migration state; standardise per module and keep shared fixtures framework-agnostic.
solid answer
~60 sBoth engines run within the same platform execution, so they share the JVM, the test classpath, JVM args and system properties, and their results merge into one report tree. That is the entire overlap. Everything behavioural is per-engine. Kotest's project configuration, isolation modes, extensions, coroutine and timeout settings, and its `kotest.tags` filtering apply only to specs; Jupiter's annotations, extension mechanism and tag expressions apply only to Jupiter tests. Neither engine's setup or filtering reaches the other's tests, which is precisely why Kotest publishes its own extension modules for framework integrations instead of reusing Jupiter's. My judgement: a mix is a perfectly reasonable **migration state** and a poor **steady state**. The cost is cognitive — two lifecycles, two ways to disable a test, two filtering systems — and it shows up worst in CI, where a tag-based selection has to be expressed twice or it silently misses half the suite. So I standardise per module, keep shared fixtures as plain classes and functions rather than engine-specific extensions, and set a migration deadline rather than letting the split calcify.
go deeper
Know that both can run in the same module and that setup written for one framework does not apply to the other.
List concretely what is shared (JVM, classpath, report) versus per-engine (lifecycle, extensions, configuration, tags).
Lead with the filtering asymmetry and its silent under-testing failure mode, and describe framework-agnostic fixtures as the mitigation.
Make it a scoped decision: standardise per module, treat the mix as a migration state with an owner and an end date, and name the cognitive and CI costs you are trading against migration risk.
## What one platform run actually gives you When a module has both kinds of tests, one platform execution hosts two engines. The shared surface is small and physical: - **One JVM** (per fork), so JVM arguments, memory settings, the system-property map and any static state are common. A test in one engine mutating a singleton is visible to the other. - **One classpath.** Both engines see the same production and test classes and the same libraries. - **One result stream.** Both engines report into the same run, so the IDE tree and the generated report contain both, and an overall pass/fail covers both. That is it. The overlap is the *container*, not the behaviour. ## What is strictly per-engine **Lifecycle.** Kotest's `beforeSpec` / `beforeTest` / `afterTest` callbacks fire only around Kotest tests. Jupiter's per-test and per-class callbacks fire only around Jupiter tests. There is no shared "before anything" hook across engines. **Extensions.** Kotest extensions/listeners are registered through Kotest (on a spec, or in project configuration) and apply to specs only. Jupiter's extension mechanism applies to Jupiter tests only. Kotest ships its own extension modules for common framework integrations precisely because the other engine's extensions cannot reach specs — a fact worth stating explicitly in an interview, since it explains an otherwise puzzling duplication in the ecosystem. **Configuration.** Kotest's project-level configuration class governs isolation mode, parallelism, default timeouts, global extensions and assertion behaviour — for Kotest tests. Jupiter has its own configuration surface for its own tests. Neither is a platform-wide setting, so "set the timeout for the module" is two separate changes. **Filtering.** This is the sharpest edge. Kotest tags are `Tag` objects selected with the `kotest.tags` system property; Jupiter tags are strings selected by Jupiter's own tag expressions. Ask for "only the fast tests" and you must express it twice, in two syntaxes, over two independently maintained tag vocabularies. Get it wrong and a CI job silently runs half the suite it thinks it ran — the failure mode is under-testing, which is invisible until something escapes. **Parallelism and ordering.** Each engine schedules its own tests according to its own settings. Assumptions like "nothing runs concurrently in this module" require configuring both. ## Deciding whether to keep the mix The honest answer is that mixing is a **transitional** state that should have an owner and an end date. **Reasons a mix is legitimate:** - Incremental migration of a large suite; rewriting everything at once is riskier than living with two engines for a quarter. - A small number of tests that genuinely need something only the other ecosystem provides, isolated and documented. - Third-party test classes you do not own that come as Jupiter tests. **Costs to weigh:** - **Cognitive load.** Two lifecycles, two ways to disable a test, two extension models, two filtering syntaxes. New joiners copy whichever example they hit first and get it wrong. - **CI selection risk.** Every tag-based or subset-based job must be expressed for both engines. - **Fixture duplication.** Setup implemented as a Jupiter extension has to be re-implemented as a Kotest extension, and the two drift. - **Diagnostic confusion.** "Why didn't my setup run?" becomes a per-file question about which engine owns the class. **The decision I'd make:** standardise **per module**, not per repository — a module is the smallest unit where the test task, the filters and the fixtures are shared, so a module that is entirely one engine has no split-brain. Where a mix persists, I would: 1. Write shared fixtures as **plain classes and functions** (builders, fake implementations, container/resource holders) invoked explicitly, not as engine-specific extensions. Plain code is the only thing both engines can use unchanged. 2. Keep any tag vocabulary mirrored and, if CI selects by tag, assert in review that both expressions were updated together — or avoid tag-based selection while the mix lasts. 3. Document, in the module's README or test package, which engine owns what and why. 4. Track the remaining Jupiter classes as a finite list with an owner, so the transitional state is visibly transitional. ## The signal an interviewer is listening for Weak answers say "they both run, so it's fine" (true of the mechanics, blind to the operational cost) or "it can't work" (false). The strong answer separates the shared container from the unshared behaviour, names filtering as the highest-risk asymmetry because its failure mode is silent, and treats standardisation as a scoped decision with a migration plan rather than a taste preference.
- Which asymmetry between the two engines is the most dangerous in CI, and why?Tag-based filtering. Kotest tags are selected through Kotest's own tag expression and Jupiter tags through Jupiter's, and neither reaches the other's tests. A job intending to run "only integration tests" can therefore run one engine's subset and silently skip the other's entirely. The failure mode is under-testing rather than a red build, so nobody notices until an untested change escapes.
- How should shared setup be written in a module that has to keep both engines for a while?As plain Kotlin — factory functions, builder objects, a resource holder with explicit start/stop — invoked from each framework's own lifecycle hook. Only ordinary code is usable unchanged by both engines; anything expressed as a Jupiter extension has to be re-implemented as a Kotest extension and the two copies will drift. The frameworks then contribute only the call site, not the logic.
saying these in an interview costs you the question
- Claiming Kotest and Jupiter tests cannot coexist in one module
- Assuming Kotest project configuration (timeouts, parallelism, isolation) also governs Jupiter tests
- Expecting one tag expression to filter both engines' tests
- Believing a Jupiter extension will provide setup for Kotest specs
- Treating an indefinite mix as a neutral choice with no operational cost