Question

Kotlin JUnit @Rule Visibility with ActivityTestRule in Android Tests

kotlinandroidtestingandroid-espresso

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.

Take Quiz

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:

class ExampleTest {

    @get:Rule
    val temporaryFolder = TemporaryFolder()
}

For Android ActivityTestRule, many codebases used:

class RadisTest {

    @Rule
    @JvmField
    val mActivityRule = ActivityTestRule(MainActivity::class.java)
}

The exact working style depends on what the framework inspects: the field, the getter, or both.

Full Android example

@RunWith(AndroidJUnit4::class)
@LargeTest
class RadisTest {

    @Rule
    @JvmField
    val mActivityRule = ActivityTestRule(MainActivity::class.java)

    @Test
    fun launchesActivity() {
        val activity = mActivityRule.activity
        assertNotNull(activity)
    }
}

This works because JUnit can now see a public field for the rule.

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

  1. The test runner starts RadisTest.
  2. JUnit scans the class for members annotated with @Rule.
  3. It finds mActivityRule.
  4. Because of @JvmField, the generated JVM bytecode contains a real field that JUnit can inspect directly.
  5. JUnit validates that the field is public.
  6. ActivityTestRule launches MainActivity before the test runs.
  7. The test method sampleTest() executes.
  8. mActivityRule.activity returns the launched activity.
  9. assertNotNull(activity) passes if the activity was created successfully.

What happens without @JvmField

@RunWith(AndroidJUnit4::class)
class RadisTest {

    @Rule
    val mActivityRule = ActivityTestRule(MainActivity::class.java)
}

Then JUnit may do this:

  1. Scan the class for @Rule.
  2. Inspect the generated field.
  3. See that the backing field is not public.
  4. Throw:
The @Rule 'mActivityRule' must be public.

So the failure is not really about Kotlin source visibility. It is about JVM-level visibility.

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.

Take Quiz

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 @field:?
  • Does the member need @JvmField?
  • Is reflection involved?

This kind of debugging is common in Android codebases.

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>()

Modern Kotlin uses:

MainActivity::class.java

Using current syntax makes code easier to read and maintain.

4. Forgetting the test runner annotation

Broken example:

class RadisTest {
    @Rule
    @JvmField
    val mActivityRule = ActivityTestRule(MainActivity::class.java)
}

This may not run as an Android instrumentation test without the correct runner.

Fix:

@RunWith(AndroidJUnit4::class)
class RadisTest {
    @Rule
    @JvmField
    val mActivityRule = ActivityTestRule(MainActivity::class.java)
}

5. Using deprecated testing APIs in new code

ActivityTestRule is older. In newer Android projects, ActivityScenarioRule is often preferred.

That does not change the Kotlin interop lesson, but it matters for modern codebases.

Comparisons

Kotlin property vs Java field

Kotlin codeJVM viewImportant detail
val name = "A"private field + public getterPublic in Kotlin does not always mean public field
@JvmField val name = "A"exposed fieldJava frameworks can access the field directly

@Rule placement options

StyleWhen it worksNotes
@Rule val rule = ...SometimesMay fail if framework wants a public field
@Rule @JvmField val rule = ...Often for field-based frameworksGood for JUnit rule field access
@get:Rule val rule = ...When framework inspects getterUseful with Kotlin annotation use-site targets

ActivityTestRule vs ActivityScenarioRule

RuleStatusTypical usage
ActivityTestRuleOlder APILegacy Android UI tests
ActivityScenarioRuleNewer APIPreferred in modern Android testing

Visibility in Kotlin vs visibility in Java reflection

IdeaKotlin meaningJava/JUnit meaning
public valPublic property accessNot necessarily a public field
@JvmFieldJVM field exposureVisible as a field to Java tools
Take Quiz

Cheat Sheet

Quick fix

@Rule
@JvmField
val mActivityRule = ActivityTestRule(MainActivity::class.java)

Why the error happens

  • Kotlin property != Java public field
  • JUnit @Rule may 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 getter
  • MainActivity::class.java -> modern Kotlin way to get Java Class

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.
  • Runtime errors like this are often interoperability issues, not syntax issues.

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 javaClass<MainActivity>()?

Use:

MainActivity::class.java
Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more