Ошибка "The @Rule must be public" в Android-тестах с Kotlin
Проблема: правила JUnit в Kotlin требуют особого подхода
Ошибка "The @Rule must be public" возникает из-за особенностей работы аннотации @Rule в Kotlin. JUnit требует, чтобы поле с этой аннотацией было публичным и не было final. В Kotlin все свойства по умолчанию являются final, а также имеют приватные поля backing field. Аннотация @JvmField решает только часть проблем, но не отменяет требования к модификатору доступа.
Решение: используйте публичное свойство с lateinit
Самый простой и правильный способ — объявить правило как публичное свойство с помощью lateinit var. Это гарантирует, что поле не будет final и будет доступно извне:
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
val activityRule = ActivityTestRule(MainActivity::class.java)
@Test
fun testButtonClick() {
onView(withId(R.id.button)).perform(click())
onView(withText("Hello")).check(matches(isDisplayed()))
}
}
Обратите внимание на аннотацию @get:Rule. Она указывает, что правило применяется к геттеру свойства, а не к полю. Это стандартный подход для Kotlin, который решает проблему с модификатором доступа и final.
Альтернативный вариант с @JvmField
Если вы предпочитаете использовать поле напрямую, можно применить @JvmField с явным указанием публичного доступа:
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Rule
@JvmField
val activityRule = ActivityTestRule(MainActivity::class.java)
// ... тесты
}
Но этот способ менее надёжен, так как в некоторых версиях библиотек может вызывать предупреждения компилятора. Рекомендую первый вариант.
Почему это работает?
@get:Ruleзаставляет JUnit смотреть на геттер, который является публичным и не-final.- Свойство
valв Kotlin не делает поле final на уровне JVM, если используется с@get:Rule. - ActivityTestRule инициализируется сразу при создании объекта тестового класса, поэтому
lateinitне нужен.
Полный пример с дополнительными правилами
Если у вас несколько правил (например, IdlingResource), используйте @get:Rule для каждого:
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
val idlingResourceRule = IdlingResourceRule()
@Test
fun testWithIdlingResource() {
// тест с ожиданием фоновых операций
}
}
Этот подход совместим с JUnit 4 и 5, а также со всеми версиями AndroidX Test. Если вы переходите на JUnit 5, используйте @RegisterExtension вместо @Rule, но для Espresso рекомендуется JUnit 4.
Надеюсь, это помогло! Если остались вопросы, пишите в комментариях.
<<Дополнительное решение: использование TestRule в функциональном стиле
Если вы предпочитаете более современный подход, можно обернуть создание ActivityTestRule в функцию и использовать @get:Rule с ленивой инициализацией:
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
val activityRule by lazy {
ActivityTestRule(MainActivity::class.java)
}
@Test
fun testButtonClick() {
// тест
}
}
Этот вариант полезен, когда вам нужно передать дополнительные параметры в конструктор правила, например, начальные Intent или флаг launchActivity:
@get:Rule
val activityRule by lazy {
ActivityTestRule(
MainActivity::class.java,
initialTouchMode = false,
launchActivity = true
)
}
Также обратите внимание на альтернативу — ActivityScenarioRule из AndroidX Test, который является более современной заменой ActivityTestRule и не требует таких трюков с аннотациями:
@get:Rule
val scenarioRule = ActivityScenarioRule(MainActivity::class.java)
ActivityScenarioRule автоматически обрабатывает жизненный цикл Activity и лучше интегрируется с Kotlin. Рекомендую использовать его в новых проектах.