Introduction: That Infuriating Error
\n\nHave you ever written a Kotlin test for Android, added an ActivityTestRule, run it — and suddenly saw that spine-chilling message: "The @Rule must be public"? If so — you are not alone. This question on Stack Overflow has gathered thousands of views, and every day dozens of developers step on the same rake.
The problem seems simple, but in Kotlin it turns into a real quest. In Java everything works like clockwork: @Rule, a public field — and done. But in Kotlin… well, you know. Today we'll figure out why this happens, and, most importantly, how to fix it once and for all.
By the way, if you prefer Python, PythonLib also has plenty of useful content about testing. But today — it's pure Kotlin and Android.
\n\nWhy Does the "The @Rule must be public" Error Occur?
\n\nLet's recall how ActivityTestRule works in JUnit. It is a Rule that launches an Activity before each test and properly finishes it after. In Java you write:
@Rule\npublic ActivityTestRule<MainActivity> activityTestRule =\n new ActivityTestRule<>(MainActivity.class);\n\n\nIt's simple: the field is public, the rule is public. In Kotlin, everything is public by default, but there's a catch. When you write:
\n\n@Rule\nval activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n\n…Kotlin generates a private field and a public getter. But JUnit requires exactly a public field, not a getter. That's the whole magic.
\n\nThe "The @Rule must be public" error appears precisely because JUnit doesn't see your field — it's private.
Solution #1: Use @get:Rule (The Most Popular)
\n\nThe simplest and most common way is to apply the annotation not to the field, but to the getter. For this, Kotlin has the @get:Rule syntax:
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\nWhat's happening here? We're saying: "Annotate the getter, not the field." JUnit sees the public getter, gains access to the rule, and everything works. Tested by hundreds of developers.
\n\nThis solution is your "Plan A". It's concise, clear, and doesn't require additional libraries.
\n\nSolution #2: Make the Field Public with @JvmField
\n\nIf you don't like the @get:Rule syntax (though you should), you can use the @JvmField annotation. It forces Kotlin to generate a public field without a 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\nIt works exactly like the Java code. The downside — slightly more code. The upside — you explicitly state that the field should be public.
\n\nPersonally, I prefer @get:Rule — it's shorter and reads more naturally for a Kotlin developer. But the choice is yours.
Solution #3: Use ActivityScenarioRule (The Modern Way)
\n\nNow let's talk about what Google recommends instead of ActivityTestRule. The androidx.test.ext:junit library has ActivityScenarioRule. It's more flexible and integrates better with modern Android testing.
With it, the same story applies — you need to use @get:Rule or @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