Ошибка @Rule в Kotlin: как исправить «The @Rule must be public» в Android тестах
Почему возникает ошибка?
Проблема в том, что фреймворк для тестирования (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.
Ответов пока нет.
Войдите чтобы оставить ответ