skip to content

Multiplatform Test Annotations

@Test, @BeforeTest, @AfterTest, and @Ignore in kotlin.test are expect declarations that map to each platform's real annotations. That is how one test source set runs on JVM, JS, and Native alike.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

What Gradle/source-set setup is required to write kotlin.test tests in common code for a multiplatform module, and how do you pick the JVM test runner?

level: middleimportance: should knowfreq 30%

basics

~20 s

Add the kotlin-test dependency to the commonTest source set and put your tests there using kotlin.test imports. For the JVM you pick JUnit 4 or 5 either by a small dependency or by calling useJUnitPlatform() on the test task.

open as a page

Map kotlin.test's @BeforeTest, @AfterTest, and @Ignore to their JUnit 5 equivalents, and explain how @Ignore behaves at the class level versus the function level.

level: middleimportance: should knowfreq 40%

basics

~10 s

@BeforeTest is like JUnit 5's @BeforeEach, @AfterTest is like @AfterEach, and @Ignore is like @Disabled. @Ignore on a class skips every test in it; on one function it skips just that test.

open as a page

How does kotlin.test make @Test in common code resolve to the right test framework on each target? Explain the expect/actual (typealias) mechanism.

level: seniorimportance: should knowfreq 35%

basics

~20 s

kotlin.test declares the annotations once in common code. For each platform it provides a matching real version, so on the JVM @Test becomes JUnit's @Test, on JS it becomes a JS runner's, and so on. One annotation, many backings.

open as a page

A team wants their shared kotlin.test suite to cover suspend functions and per-class setup uniformly across JVM, JS, and Native. What limits of the multiplatform test annotations do you flag, and how do you handle async tests?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

kotlin.test's annotations have no common way to run code once per class and no built-in support for suspend tests. For coroutines, use runTest from the coroutines-test library inside a @Test. Anything JVM-only goes in a JVM-only test source set.

open as a page