skip to content

In kotlin.test, which annotations mark a test method and its setup/teardown in common (multiplatform) code, and why aren't you using JUnit's @Test directly?

level: juniorimportance: must knowfreq 60%

answer

  1. @Test / @BeforeTest / @AfterTest / @Ignore from kotlin.test
  2. Before/After run per-test, not per-class
  3. JUnit @Test is JVM-only
  4. common code -> platform-neutral annotations
  5. @Ignore reports as skipped

basics

~20 s

Use @Test to mark a test, @BeforeTest to run setup before each test, and @AfterTest to run cleanup after each. They come from kotlin.test so the same code works on every platform, not just the JVM.

solid answer

~30 s

In common Kotlin code you import @Test, @BeforeTest, @AfterTest, and @Ignore from the kotlin.test package. @Test marks a function as a test, @BeforeTest runs before every test in the class, @AfterTest runs after every test, and @Ignore skips a test. You don't use org.junit.jupiter.api.Test directly because JUnit only exists on the JVM; common code compiles to JVM, JS, and Native, so it must reference platform-neutral annotations. kotlin.test declares these as expect annotations and, per target, maps (typealiases) them to the real framework — JUnit on JVM, Mocha/Jasmine-style on JS, the Kotlin/Native test runner on Native. Same source, many runners.

code

kotlin · 16 lines
kotlin
import kotlin.test.Test
import kotlin.test.BeforeTest
import kotlin.test.AfterTest
import kotlin.test.assertEquals

class StackTest {
    private lateinit var stack: ArrayDeque<Int>

    @BeforeTest fun init() { stack = ArrayDeque() }
    @AfterTest fun clear() { stack.clear() }

    @Test fun pushThenPop() {
        stack.addLast(7)
        assertEquals(7, stack.removeLast())
    }
}

go deeper

for a junior

Names the four annotations and knows they come from kotlin.test, not JUnit.

for a middle

Explains per-test lifecycle and why common code can't import JUnit directly.

for a senior

Articulates the expect/typealias mapping to JUnit on JVM and other runners per target.

for a principal

Frames it as the design pattern for platform-neutral test APIs and the trade-offs of the lowest-common-denominator surface.

## The problem kotlin.test solves Kotlin Multiplatform (KMP) lets you write code once in a **common** source set and compile it to several **targets**: JVM, JS, Native, Wasm. Test frameworks are platform-specific — JUnit is JVM-only, JS uses Mocha/Jasmine-style runners, Native has its own runner. If your common tests imported `org.junit.jupiter.api.Test`, they would not compile for JS or Native. `kotlin.test` is a small, platform-neutral test API shipped with Kotlin. In common code you import its annotations and they get **mapped per target** to the underlying framework. ## The four core annotations - **`@Test`** — marks a function (no parameters, returns Unit) as a test case. - **`@BeforeTest`** — marks a function to run **before each** test in the class (fresh setup per test). - **`@AfterTest`** — marks a function to run **after each** test (cleanup), even if the test failed. - **`@Ignore`** — skips a test (or a whole class) so it is reported as skipped, not run. ```kotlin import kotlin.test.Test import kotlin.test.BeforeTest import kotlin.test.AfterTest import kotlin.test.Ignore import kotlin.test.assertEquals class CalculatorTest { private lateinit var calc: Calculator @BeforeTest fun setUp() { calc = Calculator() } @AfterTest fun tearDown() { calc.close() } @Test fun adds() { assertEquals(4, calc.add(2, 2)) } @Ignore @Test fun notReadyYet() { /* skipped */ } } ``` ## Why not JUnit's @Test directly JUnit's `@Test` lives in `org.junit.jupiter.api` (JUnit 5) or `org.junit` (JUnit 4) — both **JVM-only**. Common code must stay platform-neutral, so it references `kotlin.test` annotations instead. On the JVM, kotlin.test's `@Test` is **typealiased to JUnit's** annotation under the hood, so a JUnit runner still discovers and runs your tests. ## Key point: per-test lifecycle Unlike `@BeforeClass`/`@AfterClass` (once per class), `@BeforeTest`/`@AfterTest` run **once per test method**, giving each test a clean fixture.

  • How often do @BeforeTest and @AfterTest run for a class with three @Test methods?
    Each runs three times — once before and once after each individual test. They are per-test, not per-class.
  • Does @AfterTest still run if the test throws?
    Yes. Teardown runs regardless of whether the test passed, failed an assertion, or threw — it mirrors JUnit's @AfterEach behavior.

kotlin.test is a universal power adapter: you plug into one socket (@Test) and it fits whatever outlet (JUnit/JS/Native) the target country uses.

saying these in an interview costs you the question

  • Importing org.junit.jupiter.api.Test in a common source set
  • Thinking @BeforeTest runs once per class like @BeforeClass
  • Believing @Ignore deletes or fails the test instead of skipping it
  • Saying kotlin.test only works on the JVM

context