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?
answer
- kotlin.test = common/multiplatform; assertThrows = JVM/JUnit5
- JVM kotlin-test delegates via junit/junit5/testng adapters
- Both return T and accept subtypes
- assertThrowsExactly forbids subtypes; kotlin.test has no direct equivalent
- Use assertFailsWith in commonTest
basics
~10 sassertFailsWith 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
Knows assertFailsWith is from kotlin.test and works on multiple platforms while assertThrows is JUnit-only.
Explains both return the exception and accept subtypes, and that kotlin.test is framework-neutral on the JVM.
Understands the JVM adapter delegation, the assertThrowsExactly strictness gap, and when to choose each.
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