Question
Kotlin JUnit @Rule Visibility with ActivityTestRule in Android Tests
Question
I am writing a UI test for an Android app in Kotlin using ActivityTestRule. The test compiles, but at runtime JUnit throws this error:
java.lang.Exception: The @Rule 'mActivityRule' must be public.
Here is the test class:
@RunWith(AndroidJUnit4::class)
@LargeTest
class RadisTest {
@Rule
val mActivityRule: ActivityTestRule<MainActivity> =
ActivityTestRule(MainActivity::class.java)
// ...
}
Since the property is already public in Kotlin, why does JUnit still say the @Rule must be public, and how should this be declared correctly?
Short Answer
By the end of this page, you will understand why a Kotlin property can look public but still fail JUnit's @Rule checks, how Kotlin properties differ from Java fields, and how to correctly declare ActivityTestRule in Android UI tests using @JvmField. You will also learn the broader idea of Java interoperability in Kotlin test code.
Concept
Kotlin properties are not the same thing as Java fields.
When you write this in Kotlin:
val mActivityRule = ActivityTestRule(MainActivity::class.java)
it usually creates:
- a private backing field
- a public getter method
From Kotlin's point of view, the property is public because other code can access it through the getter. But JUnit's @Rule support was originally designed for Java and often validates fields directly, not Kotlin properties.
That means JUnit inspects the generated Java-level field and sees that the field is not public, even though the Kotlin property is accessible.
To expose the property as a real public field for Java-based frameworks like JUnit, Kotlin provides @JvmField.
Correct version:
@Rule
@JvmField
val mActivityRule = ActivityTestRule(MainActivity::class.java)
Why this matters:
- Android tests often rely on Java-based frameworks like JUnit and Espresso.
- Kotlin compiles to JVM bytecode, so Java interoperability matters.
- Framework annotations may target fields, methods, or both.
- If you do not expose things in the form the framework expects, code may compile but fail at runtime.
This is a common beginner issue when using Kotlin with older Java libraries and test runners.
Mental Model
Think of a Kotlin property like a room with a receptionist.
- The field is the actual room.
- The getter is the receptionist who lets people access the room.
Kotlin says, "The room is publicly accessible because the receptionist is public."
But JUnit says, "I do not want to talk to the receptionist. I need direct access to the room itself."
@JvmField removes the receptionist layer and exposes the room directly as a public field, which is what JUnit expects for @Rule in this case.
Syntax and Examples
Basic syntax
Problematic version
@RunWith(AndroidJUnit4::class)
class RadisTest {
@Rule
val mActivityRule = ActivityTestRule(MainActivity::class.java)
}
This looks fine in Kotlin, but JUnit may fail because it checks the underlying Java field.
Correct version
@RunWith(AndroidJUnit4::class)
class RadisTest {
@Rule
@JvmField
val mActivityRule = ActivityTestRule(MainActivity::class.java)
}
@JvmField tells Kotlin to expose the property as a field directly instead of only through generated accessors.
Another example
class ExampleTest {
@get:Rule
val temporaryFolder = TemporaryFolder()
}
In some JUnit cases, annotating the getter with @get:Rule is also used because Kotlin annotations on properties can be placed on different JVM targets.
A commonly used modern Kotlin test declaration is:
Step by Step Execution
Consider this test:
@RunWith(AndroidJUnit4::class)
class RadisTest {
@Rule
@JvmField
val mActivityRule = ActivityTestRule(MainActivity::class.java)
@Test
fun sampleTest() {
val activity = mActivityRule.activity
assertNotNull(activity)
}
}
What happens step by step
- The test runner starts
RadisTest. - JUnit scans the class for members annotated with
@Rule. - It finds
mActivityRule. - Because of
@JvmField, the generated JVM bytecode contains a real field that JUnit can inspect directly. - JUnit validates that the field is public.
ActivityTestRulelaunchesMainActivitybefore the test runs.- The test method
sampleTest()executes. mActivityRule.activityreturns the launched activity.assertNotNull(activity)passes if the activity was created successfully.
Real World Use Cases
This concept shows up whenever Kotlin code interacts with Java frameworks.
Common real uses
- Android UI testing with
ActivityTestRule,GrantPermissionRule, or similar JUnit rules - Unit testing with
TemporaryFolder,ExpectedException, or custom JUnit rules - Dependency injection frameworks that inspect fields or methods using reflection
- Serialization libraries that expect fields, getters, or constructors in a specific form
- Mocking frameworks that rely on Java-visible members
Practical example
If a framework uses reflection to inspect your class, these details matter:
- Is the annotation on the field or getter?
- Is the field public?
- Did Kotlin generate accessors instead of a direct field?
In small projects this might seem rare, but in Android and JVM development it happens often.
Real Codebase Usage
In real projects, developers usually solve this with one of these patterns.
1. Expose rules as JVM fields
@Rule
@JvmField
val activityRule = ActivityTestRule(MainActivity::class.java)
This is common when the framework expects a public field.
2. Put annotations on the getter when needed
@get:Rule
val tempFolder = TemporaryFolder()
This is used when the framework reads JavaBean-style accessor methods instead of fields.
3. Prefer modern APIs when available
ActivityTestRule has largely been replaced in newer Android testing by ActivityScenarioRule.
Example:
@get:Rule
val scenarioRule = ActivityScenarioRule(MainActivity::class.java)
Modern libraries often provide better Kotlin compatibility.
4. Use guard thinking with interop
When a Java framework behaves strangely in Kotlin, check:
- Does it expect a field or a getter?
- Does the annotation need a use-site target like
@get:or ?
Common Mistakes
1. Assuming public val means a public Java field
Broken example:
@Rule
val mActivityRule = ActivityTestRule(MainActivity::class.java)
Why it fails:
- The property is public in Kotlin.
- The underlying field may still not be public for Java reflection.
Fix:
@Rule
@JvmField
val mActivityRule = ActivityTestRule(MainActivity::class.java)
2. Forgetting Kotlin annotation targets
Sometimes developers write an annotation on a property, but the Java library expects it on the getter or field.
Example:
@Rule
val tempFolder = TemporaryFolder()
Possible fix depending on framework:
@get:Rule
val tempFolder = TemporaryFolder()
3. Mixing old Kotlin syntax with modern Kotlin
Older code may show:
javaClass<MainActivity>()
Comparisons
Kotlin property vs Java field
| Kotlin code | JVM view | Important detail |
|---|---|---|
val name = "A" | private field + public getter | Public in Kotlin does not always mean public field |
@JvmField val name = "A" | exposed field | Java frameworks can access the field directly |
@Rule placement options
| Style | When it works | Notes |
|---|---|---|
@Rule val rule = ... | Sometimes | May fail if framework wants a public field |
@Rule @JvmField val rule = ... | Often for field-based frameworks |
Cheat Sheet
Quick fix
@Rule
@JvmField
val mActivityRule = ActivityTestRule(MainActivity::class.java)
Why the error happens
- Kotlin property != Java public field
- JUnit
@Rulemay validate fields directly - A public getter is not enough if the framework expects a field
Key tools
@JvmField-> expose a real JVM field@get:Rule-> put the annotation on the getterMainActivity::class.java-> modern Kotlin way to get JavaClass
Common patterns
@Rule
@JvmField
val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
val scenarioRule = ActivityScenarioRule(MainActivity::class.java)
Remember
- If a Java framework uses reflection, Kotlin/JVM details matter.
- Check whether the framework expects a field, getter, or method.
FAQ
Why does JUnit say my Kotlin @Rule is not public when it is public?
Because Kotlin exposes many properties through getter methods, not as public Java fields. JUnit may be checking the field directly.
What does @JvmField do in Kotlin?
It tells Kotlin to expose the property as a JVM field directly instead of only generating accessor methods.
Should I use @Rule or @get:Rule in Kotlin?
It depends on what the framework expects. If the framework inspects fields, @Rule with @JvmField is common. If it inspects getters, @get:Rule is appropriate.
Is ActivityTestRule still the recommended Android testing rule?
In many modern Android projects, ActivityScenarioRule is preferred. ActivityTestRule is older but still useful for understanding existing code.
Why did the code compile if it fails at runtime?
The Kotlin code is valid. The failure happens later when JUnit uses reflection and checks JVM-level member visibility.
Do all Java libraries have this problem with Kotlin properties?
Not all, but many reflection-based libraries can behave differently depending on whether they inspect fields, getters, or methods.
What is the correct modern Kotlin syntax instead of ?
Mini Project
Description
Create a small Android test class that launches an activity with a JUnit rule and verifies that the activity starts correctly. This project demonstrates the real cause of the @Rule must be public error and how to fix it using Kotlin's JVM interop features.
Goal
Write a working Kotlin instrumentation test that uses a JUnit rule without triggering the public field error.
Requirements
- Create a Kotlin test class that uses
AndroidJUnit4. - Add a rule that launches
MainActivity. - Declare the rule so JUnit can access it correctly.
- Add one test that checks the activity instance is not null.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Android AlarmManager Example: Scheduling Tasks with AlarmManager
Learn how to use Android AlarmManager to schedule tasks, set alarms, and handle broadcasts with a simple beginner example.
Android Foreground Service Notification Channels in Kotlin
Learn why startForeground fails on Android 8.1 and how to create a valid notification channel for foreground services in Kotlin.