skip to content

Kotest's test config accepts enabled, enabledIf and enabledOrReasonIf. What is the difference between the three, and why can enabled = someRuntimeCheck() behave differently from enabledIf = { someRuntimeCheck() }?

level: middleimportance: should knowfreq 32%

answer

  1. enabled = constant, evaluated at registration
  2. enabledIf = (TestCase) -> Boolean, evaluated at run time
  3. enabledOrReasonIf returns Enabled.disabled(reason)
  4. tags = policy from outside; enabledIf = capability probe
  5. skipped is not passed — watch the skip count

basics

~20 s

enabled is a plain Boolean fixed when the test is registered; enabledIf is a lambda taking the TestCase and evaluated later, when the engine decides whether to run that test; enabledOrReasonIf returns Kotest's Enabled value so the skip carries a printed reason. A value computed for enabled is frozen at registration time; the lambda is not.

solid answer

~50 s

All three live on Kotest's test config: - `enabled: Boolean` — a constant. It is evaluated while the spec body runs and the test is registered. - `enabledIf: (TestCase) -> Boolean` — a predicate given the TestCase, so it can branch on the test's own name or config, and it is invoked when the engine is about to run that test. - `enabledOrReasonIf: (TestCase) -> Enabled` — same shape, but returns `Enabled.enabled` or `Enabled.disabled("reason")`, so the skip is reported with an explanation instead of silently. The difference bites when the condition is not constant. `enabled = dockerIsRunning()` runs that check once, during spec construction, before any lifecycle hook that might start Docker has fired — so the answer can be stale or plain wrong. `enabledIf = { dockerIsRunning() }` defers the call to the moment the test is about to run. Prefer `enabledOrReasonIf` for anything environmental: a skipped test with no reason is indistinguishable from a test nobody wrote.

code

kotlin · 15 lines
kotlin
class PlatformSpec : FunSpec({

   test("parked").config(enabled = false) { }

   test("needs a container").config(
      enabledIf = { dockerAvailable() }
   ) { }

   test("needs a container, with a reason").config(
      enabledOrReasonIf = {
         if (dockerAvailable()) Enabled.enabled
         else Enabled.disabled("no docker daemon on this agent")
      }
   ) { }
})

go deeper

for a junior

Know that enabled is a plain flag and enabledIf is a lambda checked when the test is about to run.

for a middle

Explain the registration-time versus execution-time evaluation and why a probe in enabled can read stale state; know enabledOrReasonIf prints a reason.

for a senior

Draw the policy-versus-capability line between tags and enabledIf, and insist skipped counts are monitored so conditional tests cannot rot unnoticed.

for a principal

Set the rule for which conditions may be probed in code at all, and how the pipeline proves that capability-gated suites actually ran somewhere.

## Two phases, one distinction A Kotest spec has two phases. First the spec body executes, calling the registration builders and building the list of test cases with their config. Then the engine executes those test cases. `enabled` is a value supplied in phase one; `enabledIf` and `enabledOrReasonIf` are functions the engine calls in phase two. Every practical difference falls out of that. ## enabled `test("x").config(enabled = false) { ... }` is the explicit form of the disable you would otherwise write as `xtest` or a `!` name prefix. It accepts any Boolean expression, which is where the trap lives: `enabled = System.getenv("CI") != null` is fine because the environment is stable, but `enabled = database.isReachable()` is a live probe run during spec construction. At that moment, hooks that provision the environment may not have run yet, and with some isolation settings the spec body runs more than once, so the probe fires more often than you expect. A constant condition belongs in `enabled`; a live one does not. ## enabledIf `enabledIf` takes the `TestCase` and returns a Boolean, and is invoked when the engine reaches that test. Two consequences: the condition sees the world as it is at execution time, and it can inspect the test itself — for example enabling only tests whose name matches a pattern, which lets you write one conditional rule and apply it across a spec rather than repeating a flag per test. ## enabledOrReasonIf Same timing and same input, but it returns Kotest's `Enabled` type: `Enabled.enabled` or `Enabled.disabled("no Docker daemon on this agent")`. The reason surfaces in the output, which turns a mute skipped entry into a diagnosis. This matters most exactly where conditional execution is used most — environment-dependent tests — because the whole failure mode of conditional tests is that they quietly stop running and nobody notices for months. ## Choosing between these and tags There is a design line worth stating in an interview. Tags are decided **outside** the code, by whoever launches the run: the job says `-Dkotest.tags="!Docker"`. `enabledIf` is decided **inside** the code, by probing the machine. Tags are better for policy ("the fast PR job does not run Docker tests") because the decision is visible in the job definition and is deterministic across machines. `enabledIf` is better for capability ("this test needs something the current runtime may not have"), where only the running process can know. The anti-pattern is using `enabledIf` for policy: the suite silently shrinks depending on the machine it lands on, green means nothing, and there is no artifact recording what was skipped. If you do it anyway, always use `enabledOrReasonIf` so the reason is printed, and consider asserting somewhere that the count of environment-skipped tests is zero on the machines that are supposed to have the capability. ## Reporting All three produce a skipped/ignored result rather than a pass — a disabled test is not a green test, and treating it as one is how conditional execution rots a suite. Watch skipped counts in CI the same way you watch failures, and alert when they move.

  • When would you use tags instead of enabledIf for a test that needs Docker?
    Use a tag when the decision is policy owned by the job — the PR pipeline chooses not to run Docker tests, and that choice is visible in the pipeline definition and identical on every machine. Use enabledIf when the decision is a genuine capability probe only the running process can answer. Policy expressed as a probe makes the suite silently machine-dependent, which is the harder failure to notice.
  • Why is enabledOrReasonIf usually preferable to enabledIf for environment gates?
    Because it attaches a human-readable reason to the skip, so the report says why the test did not run rather than just that it did not. Environment-gated tests fail silently by nature — they stop running and nothing goes red — so the printed reason is often the only signal anyone will ever see that a capability disappeared from an agent.

saying these in an interview costs you the question

  • Claiming enabled and enabledIf are evaluated at the same moment
  • Treating a disabled or skipped test as a passing test in CI reporting
  • Putting a live environment probe in enabled and expecting it to see state set up by a lifecycle hook
  • Using enabledIf for pipeline policy that belongs in a tag filter, leaving no record of what was skipped

context