Introducción: Ese error que vuelve loco a cualquiera
\n\n¿Alguna vez has escrito un test en Kotlin para Android, añadido ActivityTestRule, lo has ejecutado y de repente has visto ese mensaje que hiela el alma: "The @Rule must be public"? Si es así, no estás solo. Esta pregunta en Stack Overflow ha acumulado miles de visitas, y cada día decenas de desarrolladores caen en la misma trampa.
El problema parece simple, pero en Kotlin se convierte en una auténtica odisea. En Java todo funciona como la seda: @Rule, un campo público, y listo. Pero en Kotlin… bueno, ya sabes. Hoy vamos a entender por qué ocurre esto y, lo más importante, cómo solucionarlo de una vez por todas.
Por cierto, si prefieres Python, en PythonLib también hay mucho contenido útil sobre testing. Pero hoy toca Kotlin y Android puro.
\n\n¿Por qué aparece el error "The @Rule must be public"?
\n\nRecordemos cómo funciona ActivityTestRule en JUnit. Es una regla (Rule) que inicia una Activity antes de cada test y la finaliza correctamente después. En Java escribes:
@Rule\npublic ActivityTestRule<MainActivity> activityTestRule =\n new ActivityTestRule<>(MainActivity.class);\n\n\nTodo simple: el campo es público, la regla es pública. En Kotlin, por defecto todo es público, pero aquí hay un truco. Cuando escribes:
\n\n@Rule\nval activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n\n…Kotlin genera un campo privado y un getter público. Pero JUnit exige exactamente un campo público, no un getter. Ahí está la magia.
\n\nEl error "The @Rule must be public" aparece precisamente porque JUnit no ve tu campo — es privado.
Solución n.º 1: Usar @get:Rule (la más popular)
\n\nLa forma más sencilla y común es aplicar la anotación no al campo, sino al getter. Para ello, en Kotlin existe la sintaxis @get:Rule:
class MainActivityTest {\n\n @get:Rule\n val activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n @Test\n fun testActivityIsNotNull() {\n assertNotNull(activityTestRule.activity)\n }\n}\n\n\n¿Qué ocurre aquí? Le decimos: "Anota el getter, no el campo". JUnit ve el getter público, obtiene acceso a la regla, y todo funciona. Probado por cientos de desarrolladores.
\n\nEsta solución es tu "plan A". Es concisa, clara y no requiere librerías adicionales.
\n\nSolución n.º 2: Hacer el campo público con @JvmField
\n\nSi no te gusta la sintaxis @get:Rule (aunque no debería ser así), puedes usar la anotación @JvmField. Esta obliga a Kotlin a generar un campo público sin getter:
class MainActivityTest {\n\n @JvmField\n @Rule\n val activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n @Test\n fun testActivityIsNotNull() {\n assertNotNull(activityTestRule.activity)\n }\n}\n\n\nFunciona exactamente igual que el código Java. La desventaja es un poco más de código. La ventaja es que indicas explícitamente que el campo debe ser público.
\n\nPersonalmente prefiero @get:Rule — es más corto y se lee de forma más natural para un desarrollador Kotlin. Pero la decisión es tuya.
Solución n.º 3: Usar ActivityScenarioRule (la forma moderna)
\n\nAhora hablemos de lo que Google recomienda en lugar de ActivityTestRule. En la librería androidx.test.ext:junit está ActivityScenarioRule. Es más flexible y se integra mejor con el testing moderno de Android.
Con él pasa lo mismo: hay que usar @get:Rule o @JvmField:
class MainActivityTest {\n\n @get:Rule\n val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)\n\n @Test\n fun testActivityIsNotNull() {\n activityScenarioRule.scenario.onActivity { activity ->\n