How does Kotest 5 locate your AbstractProjectConfig class at runtime, and what would you check if its settings appear to be ignored?
answer
- subclass AbstractProjectConfig, no-arg constructor
- package io.kotest.provided (Kotest 5 convention)
- kotest.framework.config.fqn system property
- one config per run, per module classpath
- test source set, not main
basics
~20 sWrite one class (or object) extending AbstractProjectConfig with a no-arg constructor. Kotest 5 picks it up by convention from the io.kotest.provided package on the test classpath, or from the fully qualified name given in the kotest.framework.config.fqn system property. One config applies per run.
solid answer
~50 sYou declare configuration by subclassing `AbstractProjectConfig`: ```kotlin package io.kotest.provided import io.kotest.core.config.AbstractProjectConfig class ProjectConfig : AbstractProjectConfig() { override val isolationMode = IsolationMode.InstancePerLeaf } ``` Kotest 5 finds it by convention: a subclass in the **`io.kotest.provided`** package, on the test classpath of the module being executed. You can point at any class explicitly with the `kotest.framework.config.fqn` system property instead. Kotest uses one config per run. When settings seem ignored, check in this order: is the class in `io.kotest.provided` (or is the FQN property actually reaching the JVM running the tests)? Is it on the *test* classpath of the module whose tests are running — each module's run resolves its own config, so a config in module A does nothing for module B's tests? Does it have a usable no-arg constructor? And are you overriding the real property name rather than shadowing it with a new one?
code
kotlin · 11 linespackage io.kotest.provided
import io.kotest.core.config.AbstractProjectConfig
import io.kotest.core.spec.IsolationMode
import kotlin.time.Duration.Companion.seconds
class ProjectConfig : AbstractProjectConfig() {
override val isolationMode = IsolationMode.SingleInstance
override val timeout = 30.seconds
override fun extensions() = listOf(MyGlobalListener)
}go deeper
Know that global settings live in a class extending AbstractProjectConfig placed in the io.kotest.provided package.
Add the FQN system property alternative and the one-config-per-run rule, plus the classpath/source-set checks.
Debug systematically — package, module classpath, forked-JVM properties, constructibility — and separate detection failures from precedence effects.
Set the repo-wide convention: thin per-module configs, shared settings in a test-support artefact, and documented rationale for every global default.
## What the class is `AbstractProjectConfig` is the single place to express run-wide test settings: default isolation mode, default timeouts, assertion mode, test ordering, concurrency knobs, globally registered extensions, and project-level `beforeProject`/`afterProject` callbacks. You subclass it and override the properties or functions you care about; everything you do not override keeps Kotest's default. ```kotlin package io.kotest.provided class ProjectConfig : AbstractProjectConfig() { override val isolationMode = IsolationMode.SingleInstance override val timeout = 30.seconds override fun extensions() = listOf(MyGlobalListener) override suspend fun beforeProject() { /* run-wide setup */ } } ``` The class name does not matter; the type and the location do. ## Detection in Kotest 5 Two mechanisms: 1. **Package convention.** Kotest 5 looks for a subclass of `AbstractProjectConfig` in the package `io.kotest.provided`. This is the scan-free path and the one to teach your team: put the file in a source directory whose package declaration is `io.kotest.provided`, in the test source set. 2. **Explicit FQN.** The system property `kotest.framework.config.fqn` names the config class directly, wherever it lives. Useful when you keep configuration in your own package structure, or when a shared test-support artefact supplies it. Either way, a single config is used for the run. Do not expect two config classes to merge — write one, and have it delegate to shared helpers if several teams contribute settings. ## Why it is a class and not a file Because configuration is code: you can compute a timeout from an environment variable, register different extensions on CI than locally, or build the extension list from a helper shared across modules. That expressiveness is the reason the config is not simply a properties file — though several settings can *also* be supplied through system properties or a `kotest.properties` file on the classpath, which is how CI often adjusts a run without editing code. ## Debugging "my config is ignored" Work through the chain, cheapest first. - **Package.** Is the declared package exactly `io.kotest.provided`? A file in a directory called `provided` with a different `package` line does not count — Kotest resolves by package, not by folder. - **Classpath and module.** Test runs resolve config from the classpath of the module being executed. In a multi-module repository, module B's tests do not see module A's config unless the config ships in an artefact that B depends on (and even then, only if it lands in `io.kotest.provided` or is named via the FQN property). - **Source set.** A config compiled into main rather than test may not be on the test run's classpath at all. - **Constructibility.** Kotest instantiates it reflectively; it needs to be public with a no-arg constructor (an `object` also works). - **The FQN property reaching the right JVM.** Setting a system property in a shell is not the same as it arriving in the forked JVM that runs the tests; verify with a `println(System.getProperty(...))` from a test if in doubt. - **Real override.** `override val isolationMode = …` fails to compile if the name is wrong, which is good — but a hand-written `val isolationMode` with no `override` (in an unrelated helper) silently does nothing. Prefer explicit `override`. - **Being overridden downstream.** A setting can be present and still not take effect because something more specific wins — a spec-level property or per-test `.config(...)`. Confirm by checking the spec before blaming detection. ## Practical conventions - Keep exactly one config class per module that runs tests, all in `io.kotest.provided`, all thin — a few overrides plus `extensions()` pointing at objects declared elsewhere. - Put shared behaviour in a test-support artefact and have each module's config register it, rather than trying to make one config serve every module. - Comment non-obvious global settings with *why*: a global default is invisible from the spec that it affects, so the reasoning must live somewhere a reader will find.
- In a multi-module repository, does one AbstractProjectConfig cover every module's tests?No. Each module's test run resolves configuration from its own test classpath, so a config declared in one module does not govern another module's specs. The usual arrangement is a shared test-support artefact that exposes the extensions and defaults, plus a thin config class per module that registers them. Duplicating a handful of overrides per module is the price of independent module runs.
- You set kotest.framework.config.fqn but the config still seems inert. What do you verify?That the property reaches the JVM actually executing the tests rather than the launching shell, that the fully qualified name is exact including the package, and that the named class is public with a no-arg constructor. Print the property from inside a test to confirm it arrived. If it is present and correct, the next suspect is precedence — a spec-level or per-test setting overriding the global one.
saying these in an interview costs you the question
- Assuming any class extending AbstractProjectConfig anywhere is picked up automatically in Kotest 5
- Putting the config in the main source set
- Expecting two config classes to be merged
- Believing one module's config governs the whole repository
- Concluding detection failed when a spec-level override is actually winning