Введение: Та самая ошибка, которая сводит с ума
Вы когда-нибудь писали тест на 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. Должен быть <