Compare the two ways to run a once-per-class @BeforeAll in a Kotlin JUnit 5 test, and explain when each is appropriate.
answer
- Companion + @JvmStatic = real static, keeps PER_METHOD isolation
- PER_CLASS = one instance, write lateinit fields
- Static for process-wide singletons / extensions
- PER_CLASS enables non-static @MethodSource
- PER_CLASS: reset mutable state in @BeforeEach
basics
~10 sYou can either put @BeforeAll in a companion object with @JvmStatic, making it truly static, or add @TestInstance(PER_CLASS) so a normal method works. Static suits truly shared resources; PER_CLASS suits instance-bound setup.
solid answer
~40 sOption A: a `companion object` with `@JvmStatic @BeforeAll fun ...`. This compiles to a genuine static method, runs exactly once before any instance exists, and keeps tests in the default `PER_METHOD` isolation (a fresh instance per test). It fits process-wide shared resources (a Testcontainers container, a static port) and integrates with extensions expecting static fields. Option B: class-level `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` with a non-static `@BeforeAll`. One instance backs the class, so setup can populate `lateinit` instance fields directly and you avoid the companion scope; but mutable instance state now leaks across tests unless reset in `@BeforeEach`. PER_CLASS additionally enables non-static `@MethodSource`/`@TestFactory` factories. Choose static for true singletons and isolation; choose PER_CLASS for ergonomic instance-bound setup, accepting the shared-state discipline. Mixing: a PER_CLASS class can still use a companion for genuinely static needs.
code
kotlin · 9 lines// Static singleton style
class ContainerTest {
companion object {
@JvmStatic private val pg = PostgresContainer().also { }
@JvmStatic @BeforeAll fun up() { pg.start() }
@JvmStatic @AfterAll fun down() { pg.stop() }
}
@Test fun `db is reachable`() { /* ... */ }
}go deeper
Knows both approaches exist and make @BeforeAll work in Kotlin.
Can write both correctly and knows @JvmStatic produces a real static method.
Weighs isolation vs ergonomics, @MethodSource implications, and the shared-state discipline.
Sets a team standard, accounts for Testcontainers/extension constraints, and documents when each is mandated.
## The two mechanisms ### Option A — companion object + @JvmStatic ```kotlin class DbTest { companion object { private lateinit var db: Database @JvmStatic @BeforeAll fun start() { db = Database.start() } @JvmStatic @AfterAll fun stop() { db.close() } } @Test fun `connects`() { /* uses companion db */ } } ``` `@JvmStatic` makes the companion method a real **static** JVM method, satisfying `@BeforeAll`'s static requirement. The class stays in the default **`PER_METHOD`** lifecycle, so each `@Test` still gets a **fresh instance** — maximum isolation. Shared state lives in the companion, explicitly static. ### Option B — @TestInstance(PER_CLASS) ```kotlin @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DbTest { private lateinit var db: Database @BeforeAll fun start() { db = Database.start() } // non-static @AfterAll fun stop() { db.close() } @Test fun `connects`() { /* uses instance db */ } } ``` One instance backs every test, so `@BeforeAll` is a plain member and writes directly into `lateinit` **instance** fields. No companion needed. ## Trade-off matrix | Concern | @JvmStatic companion | PER_CLASS | |---|---|---| | Test isolation | High (fresh instance/test) | Lower (one shared instance) | | Boilerplate | More (companion scope) | Less | | Shared-state risk | Contained in companion | Instance fields leak; reset in @BeforeEach | | Non-static @MethodSource | No | Yes | | Mental model | Matches Java | Kotlin-friendly | ## When to choose which - **Static (Option A):** truly process-wide singletons (Testcontainers `@Container` static fields, a shared mock server, a fixed port), or when you want strict per-test instance isolation, or when an extension expects a static field. - **PER_CLASS (Option B):** instance-bound setup where the ergonomics of writing into `lateinit` fields matter, or when you need non-static `@MethodSource`. You then **must** reset mutable instance state in `@BeforeEach`. ## Mixing A PER_CLASS class can still declare a `companion object` for things that are genuinely static (constants, a container shared across multiple test classes). The two are not mutually exclusive. ## Key APIs `@BeforeAll`/`@AfterAll`, `@JvmStatic`, `companion object`, `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`, `lateinit`, `@MethodSource`.
- Can you use a non-static @MethodSource with the companion-object approach?Only if the factory itself is @JvmStatic in the companion. Plain member @MethodSource requires PER_CLASS.
- Does @JvmStatic change the test instance lifecycle?No. It only makes the companion method static; the class stays PER_METHOD with a fresh instance per test unless you also set PER_CLASS.
Static companion is a shared utility room outside every apartment; PER_CLASS is keeping one apartment for the whole night instead of a new one per visit.
saying these in an interview costs you the question
- Claiming the two approaches are identical with no trade-offs
- Forgetting PER_CLASS sacrifices per-test instance isolation
- Saying @JvmStatic alone changes the lifecycle to PER_CLASS
- Not mentioning the @BeforeEach reset discipline for PER_CLASS shared state