引言:那个让人抓狂的错误
\n\n您是否曾经为 Android 编写过 Kotlin 测试,添加了 ActivityTestRule,运行测试——然后突然看到这个令人心寒的消息:"The @Rule must be public"?如果是这样——您并不孤单。这个问题在 Stack Overflow 上获得了数千次浏览,每天都有数十名开发者踩到同一个坑。
问题看似简单,但在 Kotlin 中却变成了一场真正的冒险。在 Java 中一切都很顺利:@Rule,公共字段——搞定。而在 Kotlin 中……嗯,您懂的。今天我们将弄清楚为什么会发生这种情况,更重要的是,如何一劳永逸地解决它。
顺便说一句,如果您更喜欢 Python,PythonLib 上也有很多关于测试的有用内容。但今天——我们只讲纯 Kotlin 和 Android。
\n\n为什么会出现 "The @Rule must be public" 错误?
\n\n让我们回顾一下 ActivityTestRule 在 JUnit 中是如何工作的。这是一个规则(Rule),它在每个测试之前启动 Activity 并在测试之后正确关闭它。在 Java 中,您这样写:
@Rule\npublic ActivityTestRule<MainActivity> activityTestRule =\n new ActivityTestRule<>(MainActivity.class);\n\n\n很简单:字段是公共的,规则是 public。而在 Kotlin 中,默认情况下所有内容都是 public,但这里有一个陷阱。当您这样写时:
\n\n@Rule\nval activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n\n……Kotlin 会生成一个私有字段和一个公共 getter。而 JUnit 要求的是 公共字段,而不是 getter。这就是全部的秘密。
\n\n错误 "The @Rule must be public" 出现的原因正是 JUnit 看不到您的字段——它是私有的。
解决方案 #1:使用 @get:Rule(最流行的方法)
\n\n最简单且最常用的方法是将注解应用于 getter 而不是字段。为此,Kotlin 提供了 @get:Rule 语法:
class MainActivityTest {\n\n @get:Rule\n val activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n @Test\n fun testActivityIsNotNull() {\n assertNotNull(activityTestRule.activity)\n }\n}\n\n\n这里发生了什么?我们是在说:"注解 getter,而不是字段"。JUnit 看到公共 getter,获得对规则的访问权限,一切正常。这已经经过数百名开发者的验证。
\n\n这个解决方案是您的"方案 A"。它简洁、易懂,并且不需要额外的库。
\n\n解决方案 #2:通过 @JvmField 使字段公开
\n\n如果您不喜欢 @get:Rule 语法(虽然不应该),可以使用 @JvmField 注解。它强制 Kotlin 生成一个没有 getter 的公共字段:
class MainActivityTest {\n\n @JvmField\n @Rule\n val activityTestRule = ActivityTestRule(MainActivity::class.java)\n\n @Test\n fun testActivityIsNotNull() {\n assertNotNull(activityTestRule.activity)\n }\n}\n\n\n它的工作方式与 Java 代码完全相同。缺点——代码稍多一些。优点——明确指定字段应该是公共的。
\n\n我个人更喜欢 @get:Rule——它更短,对于 Kotlin 开发者来说读起来更自然。但选择权在您手中。
解决方案 #3:使用 ActivityScenarioRule(现代方法)
\n\n现在让我们谈谈 Google 推荐用来替代 ActivityTestRule 的方法。在 androidx.test.ext:junit 库中,有 ActivityScenarioRule。它更灵活,并且与现代 Android 测试集成得更好。
使用它时,同样需要使用 @get:Rule 或 @JvmField:
class MainActivityTest {\n\n @get:Rule\n val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)\n\n @Test\n fun testActivityIsNotNull() {\n activityScenarioRule.scenario.onActivity { activity ->\n