skip to content

You own the test suite for a large multi-team codebase using Kotest. How do you decide which settings belong in the project-wide AbstractProjectConfig versus in individual specs?

level: principalimportance: should knowfreq 20%

answer

  1. two axes: uniformity, and loud vs silent failure
  2. strictness globals cheap, semantics globals expensive
  3. isolation mode = the trap global
  4. a default most specs override is the wrong default
  5. shared test-support artefact + thin per-module config

basics

~20 s

Globalise invariants that should be uniform and fail loudly — empty-suite failure, duplicate-name policy, assertion mode, a safety-net timeout, cross-cutting extensions. Keep locally anything whose exception carries information, and anything that changes semantics silently, such as isolation mode.

solid answer

~60 s

Two questions decide it. **Should this be uniform?** If a per-spec answer would be arbitrary — an empty suite is never acceptable, duplicate test names are never acceptable — it belongs in project config. If specs legitimately differ (a two-minute timeout for integration, milliseconds for pure unit tests), the exception is information and belongs where a reader sees it. **Does it fail loudly or change behaviour silently?** Globals that turn silent problems into failures (`failOnEmptyTestSuite`, `assertionMode`, duplicate-name policy) are low risk: the worst case is a visible error. Globals that change semantics invisibly — isolation mode above all — are high risk, because a spec relying on shared state breaks in a way whose cause is nowhere near the change. Prefer explicit per-spec declaration for those, even at the cost of repetition. Then operational judgement: a global that most specs override is the wrong global; changes to a global need a staged rollout (warn, triage, enforce) because their blast radius is the whole repository; and in a multi-module repo the settings live in a shared test-support artefact with a thin config class per module.

go deeper

for a junior

Say that suite-wide rules go in project config and spec-specific needs go in the spec.

for a middle

Give concrete examples on each side and note that per-spec settings override global defaults.

for a senior

Add the failure-visibility axis, treat isolation mode as the risky global, and describe a staged rollout.

for a principal

Own the whole policy: decision rule, discoverability cost, per-module rollback granularity, migration process, and how the arrangement holds as teams and modules multiply.

## The two axes Every candidate setting can be placed with two questions. **Axis 1 — is a per-spec answer meaningful?** Some settings have one correct answer for the whole organisation. Nobody wants a green build from zero tests; nobody wants two tests sharing a name and one silently vanishing. Others have genuinely different right answers per spec: timeouts, tags, isolation for a spec with expensive shared state. When the per-spec answer carries information, forcing uniformity destroys that information. **Axis 2 — what does getting it wrong look like?** A global that adds strictness fails loudly and locally: someone sees an error naming the exact problem. A global that changes execution semantics fails quietly and distantly: a spec that was implicitly relying on shared fixture state starts failing (or worse, starts passing for the wrong reason) after someone changed a default in a file the spec's author has never opened. Strictness globals are cheap; semantics globals are expensive. That asymmetry drives most of the policy. ## The default recommendation **Globalise** - `failOnEmptyTestSuite` — a filter that matches nothing must fail, not go green. - duplicate test name policy — duplicates hide tests and corrupt reporting. - `assertionMode` — a suite-wide quality bar, enforced. - a generous default `timeout` and a `projectTimeout` — safety nets so a hung test costs a minute, not a CI slot. - cross-cutting `extensions()` — metrics, leak detection, run-wide infrastructure. **Keep local** - real timeouts for genuinely slow specs (via `defaultTestConfig`) — the exception is documentation. - tags that describe what a spec is. - isolation mode, unless your whole codebase truly shares one convention and you are prepared to defend it. - anything only one team needs. ## Why isolation mode is the special case It is the setting people most want to globalise and the one that punishes globalising hardest. It changes how many times a spec class is instantiated, which changes what state is shared between tests, which changes whether a spec that quietly depended on ordering still works. It also multiplies the cost of every per-instance callback: a `beforeSpec` that starts a container becomes a container per test. When you change it globally you are simultaneously changing correctness assumptions and runtime cost across every spec in the repository, and the failures surface far from the change. If you must change it, do it spec by spec, or per module with the ability to roll back a module at a time. ## Discoverability is a first-class cost Project config is invisible from the spec it governs. A developer debugging a flaky test reads the spec, the fixture, maybe the SUT — not a config class in another package. Mitigations that work: keep the config short enough to read in one screen; comment each override with *why* rather than what; log run-wide extensions once at startup so their presence appears in the output; and prefer strictness settings, whose effects announce themselves, over semantics settings, whose effects do not. ## Change management Changing a global in a large suite is a migration, not an edit: 1. Measure first — how many specs are affected, and how? 2. Introduce in the least destructive mode (warn before error). 3. Triage the resulting list, fixing or explicitly exempting. 4. Enforce, so new violations cannot land. 5. Announce, because other teams' builds are the blast radius. For semantics changes, add a step: land it behind per-module configs so a failing module can revert independently. ## Multi-module reality Each module's run resolves its own config from its own test classpath. The scalable arrangement is a small test-support artefact holding the extensions and shared setting values, plus a thin `AbstractProjectConfig` subclass per module that registers them. This keeps one source of truth for behaviour while letting a single module deviate deliberately — which is also the unit at which you can safely roll out a risky change. ## What separates a strong answer A weak answer lists settings. A strong one gives the decision rule (uniformity + failure visibility), names the setting that is the exception (isolation mode) with the reason, treats discoverability as a real cost rather than a nicety, and describes changing a global as a staged migration with a rollback unit.

  • Why is a globally set isolation mode riskier than a globally set assertion mode?
    Assertion mode fails loudly and locally: a flagged test names itself and the fix is obvious. Isolation mode silently changes how much state tests share and how often per-instance callbacks run, so specs that quietly depended on shared fixtures start failing — or passing for the wrong reason — with no signal pointing at the config change. One converts silence into noise, the other converts a stable assumption into an invisible variable.
  • How do you roll out a stricter global setting across a repository without blocking every team?
    Land it in a warning mode first and collect the affected specs rather than guessing at the impact. Triage the list into genuine defects, cheap conversions, and legitimate exemptions with explicit per-spec relaxations. Then flip to enforcement so new violations cannot merge, and announce it, because other teams' builds are the blast radius. For semantics changes, keep the rollout at per-module granularity so a single module can revert.
  • What signals that a project-level default is the wrong value?
    Most specs overriding it. A default exists to express a shared decision; when the majority declares something different locally, the global is producing noise and a false sense of uniformity. Either move the global to the value the suite actually uses, or drop it and let the local declarations stand as the honest documentation of what each spec needs.

saying these in an interview costs you the question

  • Globalising isolation mode because it 'makes the suite consistent' without weighing state-sharing effects
  • Treating project config as free of discoverability cost
  • Keeping a global default that most specs override
  • Changing a global in one commit across a multi-team repository with no staged rollout
  • Assuming one config governs every module in the repository

context