TL;DR: kotlin-test-annotations-common debe declararse en commonMain del módulo que exporta la clase base; kotlin-test-junit5 va en androidMain como bridge JVM. iOS no necesita nada adicional — el stdlib de Kotlin/Native ya incluye los actuals de kotlin.test.
Al migrar un módulo de testing compartido a Kotlin Multiplatform, la primera trampa aparece cuando se intenta usar @BeforeTest y @AfterTest en commonMain. El código parece correcto — los imports de kotlin.test existen, la clase compila en Android — pero en cuanto otros módulos consumen ese artefacto y ejecutan sus tests, todo falla.
El problema: @BeforeTest no llega a los consumidores
El contexto es un módulo :core:testing que exporta una clase base CoroutineTest con setup y teardown del dispatcher:
// core/testing/src/commonMain/kotlin/.../CoroutineTest.kt
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
abstract class CoroutineTest {
val testDispatcher = StandardTestDispatcher()
@BeforeTest
fun setupCoroutines() {
Dispatchers.setMain(testDispatcher)
}
@AfterTest
fun tearDownCoroutines() {
Dispatchers.resetMain()
}
}
Los imports de kotlin.test resuelven en el IDE sin problema porque el plugin de KMP inyecta el stdlib en commonMain. Pero el plugin no inyecta kotlin-test-annotations-common automáticamente cuando el módulo es una librería que otros van a consumir. Al ejecutar los tests en los módulos consumidores, el framework de test no reconoce los callbacks y el dispatcher no se inicializa.
La solución: dependencias en el lugar correcto
Hay que separar la dependencia en dos partes según el target:
// core/testing/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
api(kotlin("test-annotations-common")) // expect declarations para @BeforeTest, @AfterTest, @Test
}
androidMain.dependencies {
api(kotlin("test-junit5")) // actual implementations sobre JUnit5
}
}
}
kotlin("test-annotations-common") contiene solo las anotaciones como expect declarations — sin implementación. Es lo que permite escribir @BeforeTest en commonMain y que el compilador lo entienda en cualquier target.
kotlin("test-junit5") es el bridge que conecta esas expect declarations con JUnit5 en el target JVM/Android. Sin él, el runner de JUnit no sabe que @BeforeTest equivale a @BeforeEach.
iOS: sin configuración adicional
En Kotlin/Native (targets iosX64, iosArm64, iosSimulatorArm64), los actuals de kotlin.test vienen incluidos en el stdlib del target. No hay que añadir ninguna dependencia extra en iosMain ni en iosTest.
El otro error silencioso: junit5.api en commonTest
Hay un segundo problema que suele aparecer junto al anterior: las dependencias de JUnit5 declaradas en commonTest de los módulos consumidores.
// ANTES — incorrecto
commonTest.dependencies {
implementation(projects.core.testing)
implementation(libs.junit5.api) // ⚠️ JUnit5 no existe en iOS
implementation(libs.junit5.extensions)
}
junit5.api y junit5.extensions son artefactos JVM puros. Declararlos en commonTest hace que Gradle intente resolverlos para el target iOS también, lo que genera errores de resolución de dependencias. La corrección es moverlos a androidUnitTest:
// DESPUÉS — correcto
commonTest.dependencies {
implementation(projects.core.testing)
// sin JUnit5 aquí
}
androidUnitTest.dependencies {
implementation(libs.junit5.api)
implementation(libs.junit5.extensions)
implementation(libs.junit5.engine)
}
Los módulos consumidores que heredan CoroutineTest ya reciben el bridge JUnit5 transitivamente desde androidMain de :core:testing vía api(kotlin("test-junit5")). Solo hace falta el engine en androidUnitTest para que el runner los descubra.
Resumen
| Dependencia | Dónde va | Por qué |
|---|---|---|
kotlin("test-annotations-common") | commonMain de :core:testing | Declara @BeforeTest, @AfterTest como expect |
kotlin("test-junit5") | androidMain de :core:testing | Bridge JVM: conecta expect con JUnit5 |
kotlin("test") | commonTest del consumidor | Incluye todo para el source set de tests |
junit5.api, junit5.extensions | androidUnitTest del consumidor | Solo JVM, no pueden ir en commonTest |
| iOS | nada | Los actuals vienen en el stdlib de Kotlin/Native |