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?
answer
- no common @BeforeAll / no parameterized tests
- @Test is Unit, can't be suspend
- runTest from kotlinx-coroutines-test (multiplatform)
- @Test fun x() = runTest { } expression body for JS await
- virtual time, not Thread.sleep
basics
~20 skotlin.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.
solid answer
~40 sThe kotlin.test common annotations (@Test/@BeforeTest/@AfterTest/@Ignore) cover only the per-test lifecycle — there is no portable @BeforeClass/@BeforeAll, no parameterized-test support, and a @Test function returns Unit, so it can't be `suspend`. For coroutine code you bridge by calling `runTest { ... }` from `kotlinx-coroutines-test` inside a normal @Test; runTest is multiplatform and drives a virtual-time TestScope, so the same async test runs on JVM/JS/Native. Per-class setup that must be portable is done with a lazily-initialized top-level/companion `val` rather than an annotation; if it's JVM-specific, drop to `src/jvmTest` and use @BeforeAll. Flag that JS tests are async-by-nature in the runner and that platform timing/threading differs, so avoid wall-clock sleeps — prefer virtual time.
code
kotlin · 15 linesimport kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.launch
class CounterTest {
@Test
fun incrementsAsync() = runTest {
var n = 0
launch { n++ } // scheduled on the TestScope
advanceUntilIdle() // run all pending coroutines (virtual time)
assertEquals(1, n)
}
}go deeper
Knows you can't just mark a test suspend and needs a helper to test coroutines.
Uses runTest inside @Test and knows there's no common per-class hook.
Explains the expression-body/TestResult await on JS, virtual time, and when to drop to jvmTest.
Designs a portable test strategy: confines platform-specific concerns, standardizes async via runTest, and reasons about deterministic timing across targets.
## What the common annotations do NOT give you - **No per-class hook.** `@BeforeTest`/`@AfterTest` are per-test only. There is no portable `@BeforeAll`/`@BeforeClass` in the common kotlin.test API because that lifecycle isn't uniform across runners. - **No parameterized tests.** JUnit 5's `@ParameterizedTest`/`@MethodSource` are JVM-only. - **`@Test` functions are plain `() -> Unit`.** They cannot be declared `suspend`, so you can't directly `await` a coroutine in the test signature. ## Testing suspend functions: runTest Use `kotlinx-coroutines-test`'s `runTest`, which is itself multiplatform: ```kotlin import kotlin.test.Test import kotlin.test.assertEquals import kotlinx.coroutines.test.runTest import kotlinx.coroutines.delay class RepoTest { @Test fun loadsUser() = runTest { // returns the TestResult from runTest val repo = UserRepo() delay(1_000) // skipped via virtual time, not real waiting assertEquals("Ada", repo.load(1).name) } } ``` - `runTest` provides a **`TestScope`** with a virtual-time scheduler, so `delay` is **skipped** rather than waited — fast and deterministic. - On JS the test runner is async; `runTest` returns a value the runner can await. Writing `@Test fun x() = runTest { ... }` (expression body) lets the platform await the returned `TestResult`. This is why the **expression-body form** matters on JS/Native. ## Portable per-class setup Without a common `@BeforeAll`, share expensive fixtures via a lazy property: ```kotlin private val sharedServer by lazy { startFakeServer() } ``` Initialization happens once on first access and is reused across tests. For teardown that must happen after the whole class, there's no portable hook — handle it per-test idempotently or confine it to a JVM-only source set with `@AfterAll`. ## When to leave common code If a test needs JVM threads, real files, JUnit 5 extensions, or parameterized cases, write it in `src/jvmTest` and use the framework directly. Keep `commonTest` strictly to the portable subset. ## Timing/threading caveat Don't `Thread.sleep` (JVM-only) or rely on wall-clock timing in common tests — platforms differ. Use coroutine **virtual time** (`advanceTimeBy`, `runCurrent`, `advanceUntilIdle`) inside `runTest` for deterministic, portable async tests.
- Why does the @Test that uses runTest typically use an expression body (= runTest { })?So the function returns runTest's TestResult, which the JS/Native async runner can await. On JVM it works either way, but the expression form is the portable idiom.
- How do you do once-per-class setup portably without @BeforeAll?Use a lazily-initialized property (by lazy or a companion val) so the fixture is built once on first access and reused across tests.
The common annotations are a carry-on bag: only the essentials fit. Coroutine tests and per-class setup are the checked luggage you bring via runTest or a lazy val.
saying these in an interview costs you the question
- Declaring @Test fun foo() as suspend and expecting it to run
- Using Thread.sleep in common tests
- Assuming a common @BeforeAll exists
- Calling runBlocking in common code (it's JVM/Native only, not JS)
- Relying on wall-clock delays instead of virtual time