Avatar ·

Ошибка "The @Rule must be public" в Android-тестах с Kotlin

Всем привет! Я новичок в Android-разработке на Kotlin и пытаюсь написать свои первые UI-тесты с использованием Espresso. По примерам из интернета я создал тестовый класс и использовал ActivityTestRule для запуска Activity. Вот мой код: ```kotlin @RunWith(AndroidJUnit4::class) class MainActivityTest { @Rule @JvmField val activityRule = ActivityTestRule(MainActivity::class.java) @Test fun testButtonClick() { onView(withId(R.id.button)).perform(click()) onView(withText("Hello")).check(matches(isDisplayed())) } } ``` Но когда я запускаю тест, получаю ошибку: "The @Rule must be public". Я перепробовал разные варианты: убрал `@JvmField`, сделал Rule публичным полем, но всё равно ошибка. В чём может быть проблема? Я использую последнюю версию AndroidX Test. Подскажите, как правильно объявить ActivityTestRule в 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. Рекомендую использовать его в новых проектах.