Avatar ·

Ошибка @Rule в Kotlin: как исправить «The @Rule must be public» в Android тестах

Всем привет! Я только начал разбираться с тестированием на Android с использованием Espresso и Kotlin. У меня есть простой тестовый класс для Activity, и я пытаюсь использовать `ActivityTestRule`. Код выглядит так: ```kotlin @RunWith(AndroidJUnit4::class) class MainActivityTest { @Rule val activityRule = ActivityTestRule(MainActivity::class.java) @Test fun testButtonClick() { onView(withId(R.id.my_button)).perform(click()) } } ``` Но когда я запускаю тест, получаю ошибку: ``` java.lang.Exception: The @Rule must be public ``` Я гуглил, пишут, что в Java нужно делать поле `public`, но в Kotlin я объявил `val` — разве это не публичное поле? Я попробовал добавить `@JvmField`, но всё равно не работает. Подскажите, как правильно объявить `ActivityTestRule` в Kotlin, чтобы тест запускался? Использую последние версии библиотек: espresso-core 3.5.1, test runner 1.5.0. Буду благодарен за подробное объяснение. <<>>

Почему возникает ошибка?

Проблема в том, что фреймворк для тестирования (JUnit4) ожидает, что поле с аннотацией @Rule будет объявлено как public (публичное поле). В Java это делается автоматически, когда вы пишете public ActivityTestRule rule. В Kotlin же по умолчанию свойства (val или var) компилируются в приватное поле + геттер (для val) или геттер/сеттер (для var). JUnit4 пытается получить доступ к полю напрямую через рефлексию, а не через геттер, и видит приватное поле — отсюда и ошибка.

Решение #1: Использовать @JvmField (рекомендуется)

Самый простой и правильный способ — аннотировать свойство @JvmField. Это заставит компилятор Kotlin создать публичное поле без геттера, как в Java:

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

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

    @Test
    fun testButtonClick() {
        onView(withId(R.id.my_button)).perform(click())
    }
}

Обратите внимание на @get:Rule — это указание применить аннотацию @Rule к геттеру, а не к полю. В некоторых версиях JUnit это может работать и без @get:, но для совместимости лучше оставить.

Решение #2: Использовать lateinit var с @JvmField

Если вы предпочитаете изменяемое поле, можно использовать lateinit var:

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @get:Rule
    @JvmField
    lateinit var activityRule: ActivityTestRule<MainActivity>

    @Before
    fun setUp() {
        activityRule = ActivityTestRule(MainActivity::class.java)
    }
    // ... тесты
}

Решение #3: Использовать ActivityScenarioRule (современный подход)

Начиная с версии androidx.test:core:1.4.0 рекомендуется использовать ActivityScenarioRule вместо ActivityTestRule. Он также работает с @JvmField, но код чище:

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @get:Rule
    @JvmField
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun testButtonClick() {
        onView(withId(R.id.my_button)).perform(click())
    }
}

Почему это работает?

  • @JvmField — говорит компилятору: «Не создавай геттер, сделай поле публичным».
  • @get:Rule — применяет аннотацию JUnit к геттеру (которого после @JvmField не существует, но это не вызывает ошибку, так как аннотация игнорируется).
  • JUnit4 находит публичное поле через рефлексию и успешно применяет правило.

Если вы используете JUnit5 (через @ExtendWith), там другая система правил, и эта проблема не возникает. Но для Espresso стандартом остаётся JUnit4.

<<>>

Альтернативный подход: использование ActivityScenario без Rule

Если вы не хотите возиться с @Rule и аннотациями, можно полностью отказаться от ActivityTestRule и использовать ActivityScenario напрямую в каждом тесте. Это даёт больше контроля и избавляет от проблем с публичными полями:

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @Test
    fun testButtonClick() {
        val scenario = ActivityScenario.launch(MainActivity::class.java)
        scenario.onActivity { activity ->
            // можно взаимодействовать с activity напрямую
        }
        onView(withId(R.id.my_button)).perform(click())
        // не забудьте закрыть сценарий
        scenario.close()
    }
}

Минус — нужно вручную управлять жизненным циклом сценария (закрывать его в @After). Плюс — нет лишних зависимостей от правил JUnit4.

Также можно использовать ActivityScenarioRule из библиотеки androidx.test.ext:junit — он решает проблему из коробки без @JvmField, если обновить зависимости:

dependencies {
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

Тогда код будет работать с обычным @get:Rule без @JvmField.