@Rule must be public в Kotlin: решение проблемы с ActivityTestRule

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Та самая ошибка, которая сводит с ума

Вы когда-нибудь писали тест на Kotlin для Android, добавляли ActivityTestRule, запускали — и вдруг видели это холодящее душу сообщение: "The @Rule must be public"? Если да — вы не одиноки. Этот вопрос на Stack Overflow собрал тысячи просмотров, и каждый день десятки разработчиков наступают на те же грабли.

Проблема кажется простой, но в Kotlin она превращается в настоящий квест. В Java всё работает как по маслу: @Rule, публичное поле — и готово. А в Kotlin… ну, вы знаете. Сегодня мы разберёмся, почему это происходит, и, главное, как это исправить раз и навсегда.

Кстати, если вы предпочитаете Python, на PythonLib тоже есть много полезного про тестирование. Но сегодня — чистый Kotlin и Android.

Почему возникает ошибка "The @Rule must be public"?

Давайте вспомним, как работает ActivityTestRule в JUnit. Это правило (Rule), которое запускает Activity перед каждым тестом и корректно завершает его после. В Java вы пишете:

@Rule
public ActivityTestRule<MainActivity> activityTestRule =
    new ActivityTestRule<>(MainActivity.class);

Всё просто: поле публичное, правило — public. В Kotlin же по умолчанию всё public, но тут есть подвох. Когда вы пишете:

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

…Kotlin генерирует приватное поле и публичный геттер. А JUnit требует именно публичное поле, а не геттер. Вот и вся магия.

Ошибка "The @Rule must be public" появляется именно потому, что JUnit не видит ваше поле — оно приватное.

Решение №1: Используем @get:Rule (самый популярный)

Самый простой и распространённый способ — применить аннотацию не к полю, а к геттеру. Для этого в Kotlin есть синтаксис @get:Rule:

class MainActivityTest {

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

    @Test
    fun testActivityIsNotNull() {
        assertNotNull(activityTestRule.activity)
    }
}

Что здесь происходит? Мы говорим: "Аннотируй геттер, а не поле". JUnit видит публичный геттер, получает доступ к правилу, и всё работает. Проверено сотнями разработчиков.

Это решение — ваш "план А". Оно лаконичное, понятное и не требует дополнительных библиотек.

Решение №2: Делаем поле публичным через @JvmField

Если вы не любите синтаксис @get:Rule (хотя зря), можно использовать аннотацию @JvmField. Она заставляет Kotlin генерировать публичное поле без геттера:

class MainActivityTest {

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

    @Test
    fun testActivityIsNotNull() {
        assertNotNull(activityTestRule.activity)
    }
}

Работает ровно так же, как Java-код. Минус — чуть больше кода. Плюс — явно указываете, что поле должно быть публичным.

Лично я предпочитаю @get:Rule — он короче и читается естественнее для Kotlin-разработчика. Но выбор за вами.

Решение №3: Используем ActivityScenarioRule (современный способ)

А теперь давайте поговорим о том, что Google рекомендует вместо ActivityTestRule. В библиотеке androidx.test.ext:junit есть ActivityScenarioRule. Он более гибкий и лучше интегрируется с современным Android-тестированием.

С ним та же история — нужно использовать @get:Rule или @JvmField:

class MainActivityTest {

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

    @Test
    fun testActivityIsNotNull() {
        activityScenarioRule.scenario.onActivity { activity ->
            assertNotNull(activity)
        }
    }
}

Преимущества ActivityScenarioRule:

  • Поддержка lifecycle-тестов (можно переключать состояния Activity)
  • Лучшая работа с Espresso и UI-тестами
  • Активная поддержка от Google (в отличие от ActivityTestRule, который помечен как deprecated в некоторых контекстах)

Если вы начинаете новый проект — используйте ActivityScenarioRule. Если поддерживаете старый — ActivityTestRule тоже сойдёт, но рано или поздно придётся мигрировать.

Что ещё важно знать?

Ошибка "The @Rule must be public" может возникнуть не только с ActivityTestRule, но и с любыми другими JUnit Rules. Например, с MockitoRule, TemporaryFolder и т.д. Лечится так же — @get:Rule или @JvmField.

Вот небольшой чек-лист, чтобы избежать этой ошибки:

  • Всегда используйте @get:Rule для всех Rule-полей в Kotlin
  • Не забывайте про импорты: import org.junit.Rule
  • Проверьте, что ваше поле не private (в Kotlin по умолчанию public, но лучше перепроверить)
  • Если используете @JvmField, порядок аннотаций важен: сначала @JvmField, потом @Rule

Практический пример: полный тест-класс

Давайте соберём всё вместе. Вот полный пример теста для MainActivity с использованием ActivityScenarioRule и @get:Rule:

import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertNotNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

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

    @Test
    fun testActivityIsNotNull() {
        activityScenarioRule.scenario.onActivity { activity ->
            assertNotNull(activity)
        }
    }

    @Test
    fun testActivityTitle() {
        activityScenarioRule.scenario.onActivity { activity ->
            val title = activity.title
            assertNotNull(title)
        }
    }
}

Этот тест:

  • Запускает MainActivity перед каждым тестом
  • Проверяет, что Activity не null
  • Проверяет, что заголовок Activity не пустой

Просто, чисто, работает без ошибок.

Частые ошибки и их решения

Давайте разберём ещё несколько типичных ситуаций, с которыми сталкиваются новички.

Ошибка: "Rule must be public" даже с @get:Rule

Проверьте, что вы импортировали правильный @Rule. Должен быть <

Блоги

Рекомендаций по книгам