skip to content

Across the kotlin.test multiplatform API, how does assertFailsWith stay portable, and how does it compare to JUnit's assertThrows when running on the JVM?

level: seniorimportance: nice to knowfreq 28%

answer

  1. kotlin.test = common/multiplatform; assertThrows = JVM/JUnit5
  2. JVM kotlin-test delegates via junit/junit5/testng adapters
  3. Both return T and accept subtypes
  4. assertThrowsExactly forbids subtypes; kotlin.test has no direct equivalent
  5. Use assertFailsWith in commonTest

basics

~10 s

assertFailsWith is part of kotlin.test, so the same test code runs on JVM, JS, and Native. JUnit's assertThrows is JVM-only. Both check a thrown type and return the exception, but only kotlin.test is multiplatform.

solid answer

~40 s

`assertFailsWith` is declared in the common `kotlin.test` API and is available on every Kotlin target (JVM, JS, Native, Wasm). On the JVM, the `kotlin-test` runner can be backed by JUnit 4, JUnit 5, or TestNG via the framework-adapter artifacts, but your test source stays framework-agnostic — you write `kotlin.test` assertions and annotations (`@Test`, `@BeforeTest`) and the build maps them. By contrast, `org.junit.jupiter.api.Assertions.assertThrows<T>` is JVM/JUnit-5 only and ties the test to JUnit. Both return the caught exception (`assertThrows` returns `T`, `assertFailsWith` returns `T`) and both accept subtypes. Choose `assertFailsWith` for shared/common test code or to stay framework-neutral; `assertThrows` is fine in JVM-only modules already committed to JUnit 5, where you might also use its `assertThrowsExactly` to forbid subtypes — a strictness option kotlin.test does not expose directly.

go deeper

for a junior

Knows assertFailsWith is from kotlin.test and works on multiple platforms while assertThrows is JUnit-only.

for a middle

Explains both return the exception and accept subtypes, and that kotlin.test is framework-neutral on the JVM.

for a senior

Understands the JVM adapter delegation, the assertThrowsExactly strictness gap, and when to choose each.

for a principal

Guides multiplatform test architecture, deciding where common vs JVM-specific assertions belong and how framework adapters are wired in the build.

## Why `assertFailsWith` is portable **`kotlin.test`** is a multiplatform library: it has a **common** API plus platform implementations. `assertFailsWith` is part of that common surface, so identical test code compiles and runs on JVM, JS, Native, and Wasm. This is what lets a Kotlin Multiplatform project share failure-path tests in `commonTest`. ## The JVM adapter layer On the JVM, `kotlin-test` does not reimplement a runner — it **delegates** to an existing framework chosen at build time via an artifact: - `kotlin-test-junit` (JUnit 4) - `kotlin-test-junit5` (JUnit 5 / Jupiter) - `kotlin-test-testng` (TestNG) The adapter maps `kotlin.test` annotations (`@Test`, `@Ignore`, `@BeforeTest`, `@AfterTest`) onto the framework's, so your *source* never imports framework classes. `assertFailsWith` itself is pure Kotlin assertion logic regardless of adapter. ## Comparison with JUnit 5 `assertThrows` ```kotlin // kotlin.test — multiplatform, framework-neutral import kotlin.test.assertFailsWith val e1 = assertFailsWith<IllegalStateException> { svc.call() } // JUnit 5 — JVM only, ties test to Jupiter import org.junit.jupiter.api.Assertions.assertThrows val e2 = assertThrows(IllegalStateException::class.java) { svc.call() } ``` | Aspect | `assertFailsWith` | JUnit5 `assertThrows` | |---|---|---| | Targets | JVM, JS, Native, Wasm | JVM only | | Coupling | framework-neutral | requires JUnit 5 | | Returns exception | yes (`T`) | yes (`T`) | | Subtypes pass | yes | yes | | Forbid subtypes | not directly | `assertThrowsExactly` | ## Strictness nuance Both accept **subtypes** by default. JUnit 5 additionally offers **`assertThrowsExactly`**, which fails if a subtype (rather than the exact class) is thrown. `kotlin.test` has no built-in exact-match variant; if you need exactness, capture the exception and assert `ex::class == Expected::class` yourself. ## Choosing - **Shared/common test code, or want to switch JVM frameworks freely:** `assertFailsWith`. - **JVM-only module already on JUnit 5, or need exact-type strictness:** `assertThrows` / `assertThrowsExactly`. ## Summary `assertFailsWith` buys multiplatform portability and framework neutrality; `assertThrows` buys JUnit-5-specific features at the cost of JVM lock-in.

  • If you need to forbid subtypes (exact type match) with kotlin.test, how do you do it?
    Capture the returned exception and assert its runtime class explicitly, e.g. assertEquals(Expected::class, ex::class), since kotlin.test has no assertThrowsExactly equivalent.
  • Why doesn't your kotlin.test source import JUnit even when running on JUnit 5?
    The kotlin-test-junit5 adapter maps kotlin.test annotations onto Jupiter at build time, so source stays framework-neutral while execution uses JUnit 5.

saying these in an interview costs you the question

  • Believing assertThrows is multiplatform
  • Thinking kotlin.test reimplements a JVM runner rather than delegating
  • Assuming assertFailsWith forbids subtypes
  • Importing JUnit classes into commonTest
  • Not knowing which kotlin-test-* artifact selects the JVM framework

context