skip to content

Why does @BeforeAll often fail in a Kotlin JUnit 5 test, and how does @TestInstance(PER_CLASS) fix it?

level: middleimportance: must knowfreq 65%

answer

  1. Default = PER_METHOD = new instance per test
  2. @BeforeAll must be static in PER_METHOD
  3. PER_CLASS = one instance, non-static @BeforeAll ok
  4. Reset shared state in @BeforeEach
  5. Also enables non-static @MethodSource

basics

~10 s

By default JUnit needs @BeforeAll to be static, but Kotlin classes don't have plain static methods. Adding @TestInstance(PER_CLASS) lets JUnit reuse one instance so a normal (non-static) @BeforeAll works.

solid answer

~40 s

JUnit 5's default lifecycle is `PER_METHOD`: a fresh test instance per `@Test`. Because `@BeforeAll`/`@AfterAll` run once for the whole class, JUnit requires them to be `static` in that mode. Kotlin has no `static` keyword, so you'd otherwise move the method into a `companion object` and annotate it `@JvmStatic`. The cleaner Kotlin fix is class-level `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`: JUnit then creates a single instance for all tests, which makes non-static `@BeforeAll fun setup()` legal. The trade-off is that mutable instance state is now shared across tests, so you must reset it in `@BeforeEach` or keep it immutable. PER_CLASS also enables non-static `@MethodSource` factories for parameterized tests. You can set the default globally via `junit.jupiter.testinstance.lifecycle.default=per_class`.

code

kotlin · 10 lines
kotlin
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ConfigTest {
    private lateinit var config: Config
    @BeforeAll fun load() { config = Config.load() }   // non-static, legal
    @Test fun `has a port`() { assert(config.port > 0) }
}

go deeper

for a junior

Knows @BeforeAll runs once and that PER_CLASS makes it work without static.

for a middle

Explains the PER_METHOD vs PER_CLASS instance lifecycle and the companion-object alternative.

for a senior

Flags shared mutable state risk, resets in @BeforeEach, and knows PER_CLASS also enables non-static @MethodSource.

for a principal

Decides the team default (per-class via junit-platform.properties) weighing isolation vs Kotlin ergonomics.

## JUnit 5 test instance lifecycle JUnit 5 has two lifecycle modes, controlled by `@org.junit.jupiter.api.TestInstance`: - **`Lifecycle.PER_METHOD`** (the default): JUnit constructs a *new* instance of the test class before each `@Test` method. This isolates tests from each other's instance state. - **`Lifecycle.PER_CLASS`**: JUnit constructs *one* instance and reuses it for every test in the class. ## Why @BeforeAll needs static by default `@BeforeAll` and `@AfterAll` run **once per class**, outside the lifetime of any single test instance. In `PER_METHOD` mode there is no single shared instance to host them, so JUnit requires these methods to be **`static`**. Kotlin has no `static` keyword. The Java-style workaround is a `companion object` with `@JvmStatic`: ```kotlin class OldStyleTest { companion object { @JvmStatic @BeforeAll fun bootDatabase() { /* once */ } } } ``` This is verbose and pushes shared setup into a different scope. ## The idiomatic Kotlin fix Annotate the class with `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`. Now one instance backs all tests, so `@BeforeAll` can be a **regular member function**: ```kotlin import org.junit.jupiter.api.* @TestInstance(TestInstance.Lifecycle.PER_CLASS) class RepositoryTest { private lateinit var server: FakeServer @BeforeAll fun startServer() { server = FakeServer().also { it.start() } } @AfterAll fun stopServer() { server.stop() } @BeforeEach fun reset() { server.clear() } // reset shared state! @Test fun `serves a record`() { /* ... */ } } ``` ## The catch: shared mutable state Because the instance is reused, any mutable field persists across tests. This can cause **order-dependent flakiness**. Defend by resetting in `@BeforeEach`, or keeping fields immutable. ## Bonus: @MethodSource PER_CLASS also lets `@MethodSource` argument-provider factories be **non-static** member functions, which is otherwise impossible in Kotlin without `@JvmStatic` in a companion. ## Global default Set `junit.jupiter.testinstance.lifecycle.default=per_class` in `junit-platform.properties` to make it the default for all tests.

  • What is the risk of using PER_CLASS?
    Mutable instance fields are shared across all tests, creating possible order dependence; reset them in @BeforeEach or keep them immutable.
  • What is the alternative to PER_CLASS for @BeforeAll in Kotlin?
    Put the method in a companion object and annotate it @JvmStatic @BeforeAll, which makes it a real static method.

PER_METHOD is renting a fresh kitchen for every dish; PER_CLASS is keeping one kitchen all night and wiping the counters between dishes.

saying these in an interview costs you the question

  • Saying Kotlin has a static keyword
  • Not mentioning the shared-state trade-off of PER_CLASS
  • Confusing @BeforeAll (once per class) with @BeforeEach (before each test)
  • Claiming @BeforeAll never needs static in any mode

context