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?
answer
- Kotest = its own TestEngine, not a Jupiter dialect
- Jupiter annotations = inert metadata inside specs
- beforeTest / beforeSpec instead of @BeforeEach
- config(enabled=false), xtest, !prefix, @Ignored on the spec
- Kotest Tag objects + kotest.tags, independent of Jupiter tags
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.
solid answer
~50 sKotest does not run on top of Jupiter — the `kotest-runner-junit5` artifact contributes Kotest's **own** TestEngine to the JUnit Platform, sitting alongside Jupiter rather than inside it. Annotations such as `@BeforeEach`, `@AfterEach`, `@Disabled` and `@Tag` are Jupiter vocabulary, read by the Jupiter engine; Kotest's engine never looks at them, so inside a spec they are inert metadata. The Kotest equivalents are all DSL or Kotest-specific: - lifecycle: `beforeTest` / `afterTest` / `beforeSpec` / `afterSpec` callbacks, or extensions/listeners registered on the spec or project config; - disabling one test: `.config(enabled = false)`, an `x`-prefixed builder such as `xtest` / `xcontext`, or a `!` name prefix; - disabling a whole spec: Kotest's `@Ignored` annotation on the class; - tagging: Kotest `Tag` objects via `.config(tags = …)`, filtered with the `kotest.tags` system property. This is the single most common Kotest setup surprise — the annotations compile, so nothing warns you.
code
kotlin · 12 linesclass OrderSpec : FunSpec({
// @BeforeEach / @Disabled / @Tag here would be inert - Kotest's engine never reads them
beforeTest { resetDatabase() }
afterSpec { closePool() }
test("charges the card").config(enabled = false) { }
xtest("temporarily parked") { }
test("!also parked") { }
})
@Ignored // io.kotest.core.annotation.Ignored - the spec-level way to skip everything
class WorkInProgressSpec : FunSpec({ })go deeper
Say clearly that Kotest is its own engine and name at least the lifecycle and disabling equivalents.
Explain why the annotations are silently inert and translate lifecycle, disabling, and tagging into Kotest's vocabulary accurately.
Add that Jupiter extensions are equally unreachable and that tag filtering is a separate, independent system per engine.
Set the team policy: no Jupiter imports in spec files, deliberate per-annotation translation during migration, and an explicit decision about which engine a module standardises on.
## The root cause: a separate engine The JUnit Platform is a host that can run several **test engines** in one run. JUnit Jupiter is one engine. Kotest ships another: the `kotest-runner-junit5` artifact contributes Kotest's own engine, which discovers and executes Kotest spec classes. Annotations are not a platform-wide language. `@Test`, `@BeforeEach`, `@AfterEach`, `@Disabled`, `@Tag`, `@ExtendWith` and friends belong to Jupiter and are interpreted by the Jupiter engine. Kotest's engine implements its own discovery and execution and never inspects them. So inside a `FunSpec` (or any other spec style) those annotations are just compiled metadata that nothing reads — which is exactly why the failure is silent. There is no error, no warning, no unresolved reference; the code compiles and the annotation does nothing. The same reasoning applies to Jupiter's extension mechanism: an `@ExtendWith` on a spec class is not honoured, which is why Kotest publishes its own extension modules for framework integrations instead of reusing Jupiter extensions. A related point about `@Test` specifically: Kotest's engine ignores it outright. Whether the Jupiter engine happens to notice such a method is Jupiter's own discovery concern, but either way that method is not part of the spec — it does not participate in the spec's DSL nesting, its lifecycle callbacks, or its configuration. Treat any Jupiter annotation inside a spec as a mistake, not as a partially working feature. ## The Kotest vocabulary, item by item **Declaring tests.** Tests are lambdas registered by DSL builders, not annotated methods: `test("name") { }`, `should("name") { }`, `describe/it`, `given/when/then`, depending on the spec style. Nesting comes from nesting the builders. **Lifecycle.** Instead of `@BeforeEach`/`@AfterEach`, register callbacks inside the spec: - `beforeTest { }` / `afterTest { }` — around each test case; - `beforeSpec { }` / `afterSpec { }` — around the whole spec; - `beforeEach` / `afterEach` variants for leaf-level callbacks in styles with containers. For cross-cutting behaviour, write a Kotest extension/listener and register it on the spec (via the spec's `extensions`) or globally in project config, rather than reaching for a Jupiter extension. **Disabling.** Several Kotest-native ways, in rough order of preference: - `test("name").config(enabled = false) { }` — explicit, and `enabledIf` / `enabledOrReasonIf` allow conditions; - `xtest("name") { }`, `xcontext("name") { }`, `xdescribe("…") { }` — the `x`-prefixed builders that disable a block, familiar from other JS-style frameworks; - a `!` prefix on the test name — a quick, very visible temporary disable; - `@Ignored` (Kotest's own annotation, `io.kotest.core.annotation.Ignored`) on the **spec class** — the spec-level equivalent of Jupiter's `@Disabled`. **Focusing.** The mirror image: prefixing a **top-level** test name with `f:` focuses it, so only focused tests in that spec run. Handy for local iteration; never commit it. **Tagging and filtering.** Kotest tags are `Tag` objects, applied with `.config(tags = setOf(MyTag))` (or via annotations Kotest provides), and selected at run time with the `kotest.tags` system property using an include/exclude expression. Jupiter's `@Tag` strings and Jupiter tag expressions do not select Kotest tests, and Kotest's tag expression does not select Jupiter tests — the two filtering systems are independent because the engines are independent. ## Why this question is asked so often It is the highest-frequency real-world Kotest stumble, because everything about it looks like it should work: the annotations are on the test classpath, the IDE autocompletes them, the code compiles, and the test run is green. The candidate who can say "Kotest is a separate engine, not a Jupiter dialect" has understood the integration model; the candidate who says "you need to configure the platform differently" has not. ## Practical hygiene - Ban Jupiter annotation imports from spec files in review; they signal a misunderstanding even when harmless. - When migrating a Jupiter test class to a spec, translate every annotation deliberately rather than leaving it in place "in case it still works". - Remember that anything a Jupiter extension used to do for you must be re-done with a Kotest extension.
- How do you tag and then filter Kotest tests, given Jupiter's @Tag does nothing?Define a Kotest `Tag` object and attach it with `.config(tags = setOf(MyTag))` on the tests or specs concerned. At run time select with the `kotest.tags` system property, whose expression supports including and excluding tag names. Because the engines are independent, this filters only Kotest tests — Jupiter tests in the same module are unaffected by it, and Jupiter's own tag expressions are unaffected by yours.
- A team migrating from Jupiter relied on an @ExtendWith extension for setup. What is the Kotest equivalent?Jupiter's extension mechanism is not honoured inside specs, because Kotest's engine does not read Jupiter annotations. You reimplement the behaviour as a Kotest extension/listener and register it on the spec or in project configuration so it applies globally. For common framework integrations, Kotest publishes its own extension modules precisely because the Jupiter ones cannot reach specs.
saying these in an interview costs you the question
- Believing Kotest runs on top of the Jupiter engine and therefore understands its annotations
- Assuming @Disabled inside a spec skips the test but is being overridden by configuration
- Expecting Jupiter tag expressions to filter Kotest tests
- Thinking @ExtendWith extensions apply to Kotest specs
- Concluding the build is misconfigured rather than that the annotation is simply not read