mcpField
U
using-robolectric-correctly

using-robolectric-correctly

github.com/skydoves测试与质量33968 次安装22 周前更新
代码仓库

关于 using-robolectric-correctly

面向 Android 开发者的 Robolectric 测试指南,介绍如何在 JVM 上运行依赖 Android 框架的测试。内容涵盖运行器选择、模拟 SDK 配置、shadow、Looper 控制,以及主机与设备间的测试共享。

主要功能

  1. 01选择 AndroidJUnit4 或 Robolectric 运行器
  2. 02用 @Config 配置 SDK 版本
  3. 03通过 shadow 模拟 Android 系统类
  4. 04清空暂停状态的 Looper 队列
  5. 05组织主机和设备测试源码
  6. 06启用 Android 资源加载

适用场景

  1. 01在 JVM 上测试 Android 框架代码
  2. 02修复未模拟方法或资源缺失导致的测试失败
  3. 03控制测试中的 Handler 和 Looper 任务
  4. 04在 JVM 与设备间运行共享测试逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/skydoves/android-testing-skills --skill using-robolectric-correctly

安装后在 Claude Code 中输入/using-robolectric-correctly确认已加载。

详细介绍

正确使用 Robolectric——在 JVM 上运行 Android,避免陷阱

Robolectric 通过替换为纯 Java 实现的系统类(“影子”),在 JVM 上模拟 Android。它运行很快(比模拟器快 10–100 倍),但不是模拟器:没有真正的 RenderThread、Binder 或 GPU。本指南明确了运行器的选择、@Config 矩阵、清空 Looper 的步骤,以及 AGP 7.2+ 中 sharedTest 的注意事项。关于在 Robolectric 上运行 Compose 的具体内容,请参阅 ../../../compose/synchronization/controlling-the-test-clock/SKILL.md。

适用场景

  • 被测类导入了 android.*(Context、Resources、View、PackageManager、Looper),开发者希望在几秒内于 JVM 上测试,而不是花几分钟在模拟器上测试。
  • 单元测试失败并提示 RuntimeException: Method ... not mocked(默认的纯 Android JAR 会导致此问题)——开发者需要 Robolectric 的影子实现。
  • 单元测试失败并提示 Resources$NotFoundException——缺少 testOptions.unitTests.includeAndroidResources。
  • 开发者询问“AndroidJUnit4 和 RobolectricTestRunner,应该选哪个?”。
  • 在 Robolectric 默认的 LooperMode.PAUSED 下,Handler.postDelayed / Looper 队列无法推进。
  • 开发者询问如何在 JVM 和设备上运行同一份测试源代码(sharedTest / androidHostTest+androidDeviceTest)。
  • AGP 7.2+ 后出现构建错误:“Source directory ... already added to source set”,原因是经典的 sharedTest srcDir 共享模式。

不适用场景

  • 被测类完全没有导入 android.*。Robolectric 会白白增加 1–3 秒的类加载开销——请使用纯 JUnit4。参阅 ../../runner/configuring-junit4-on-android/SKILL.md。
  • 测试要验证像素正确性、波纹动画、截图差异、无障碍服务或 RenderThread 时序。请在模拟器/设备上运行。参阅 ../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md。
  • 测试涉及协程和 runTest,且不需要 Android 框架。请使用 ../../coroutines/testing-coroutines-with-runtest/SKILL.md。
  • 测试涉及 Compose UI 渲染。主机测试环境由 ../../../compose/setup/configuring-test-dependencies/SKILL.md 配置,时钟语义请参阅 ../../../compose/synchronization/controlling-the-test-clock/SKILL.md。

前置条件

testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.x")
testImplementation("androidx.test:core:1.7.0")              // ApplicationProvider works on both runtimes
testImplementation("androidx.test.ext:junit:1.3.0")         // androidx.test.ext.junit.runners.AndroidJUnit4

(docs/CORPUS.md §G.1;R7。)

android {
    testOptions {
        unitTests {
            includeAndroidResources = true                  // non-negotiable
        }
    }
}

只要有任何测试(或任何由测试加载的类)访问 R.*、Resources、布局、主题或 res/ 中的内容,就必须启用 includeAndroidResources。否则,测试启动时资源查找会抛出异常(R7)。

Robolectric 4.x 支持从 SDK 21 到最新稳定版的 SDK。androidx Compose 主机测试将最低版本固定为 23——请以此作为唯一可信来源:

// androidx/compose/ui/ui-test/src/androidHostTest/.../Constants.kt
internal const val RobolectricMinSdk = 23

(R7。)

运行器选择——AndroidJUnit4 还是 RobolectricTestRunner

两种运行器都可用;它们对 @Config 的处理方式相同。

运行器适用场景
androidx.test.ext.junit.runners.AndroidJUnit4**默认选择。**这是一个路由器:在 JVM 上将测试委派给 Robolectric,在设备上则委派给真正的 AndroidJUnit4ClassRunner。这是 sharedTest 模式的关键。
org.robolectric.RobolectricTestRunner仅当测试绝不能在设备上运行时使用——例如,直接使用 shadowOf(Looper.getMainLooper()).idle() 等 Robolectric 专用影子 API,或需要使用 ParameterizedRobolectricTestRunner 等 Robolectric 子类。

(R7;docs/CORPUS.md §G.6。)

import androidx.test.ext.junit.runners.AndroidJUnit4
@RunWith(AndroidJUnit4::class)
@Config(minSdk = 23)
class FooTest { /* runs under Robolectric on JVM, real runner on device */ }

import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33])
class LooperBoundTest { /* uses ShadowLooper APIs directly — JVM only */ }

重要:androidx.test.runner.AndroidJUnit4(runner 包,而非 ext.junit.runners)已标记为 @Deprecated(docs/CORPUS.md §G.2)。始终从 androidx.test.ext.junit.runners 导入。

@Config — sdk、qualifiers、application

@Config 可按类或方法配置模拟的 Android 环境。方法级配置会覆盖类级配置;子类会继承配置,也可以覆盖配置。

// Single SDK
@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
class FooTest

// SDK matrix — runs once per listed SDK
@Config(sdk = [21, 28, 33])
class BarTest

// Range — runs once at the project's targetSdk if it is >= minSdk
@Config(minSdk = 23)
class BazTest

// Per-method override
class QuxTest {
    @Test @Config(sdk = [Build.VERSION_CODES.TIRAMISU])
    fun onTiramisuOnly() { /* ... */ }

    @Test @Config(qualifiers = "fr-rFR-w820dp")           // locale + screen
    fun frenchLayout() { /* ... */ }

    @Test @Config(qualifiers = "+night")                   // additive: keep, force night
    fun darkColors() { /* ... */ }
}

// Override the Application instance
@Config(application = MyTestApplication::class)
class WithFakeApp

(R7。)

限制:同一个注解中不能同时使用 sdk 和 minSdk/maxSdk。minSdk 和 maxSdk 可以组合使用。

CI 阶段的经验之谈:在提交门禁上优先使用 @Config(sdk = [<one SDK>]);将多 SDK 矩阵留给合并门禁。矩阵中的每个 SDK 都会重新运行整个类。

Shadows

Shadow 是适用于 JVM 的 Android 系统类伪实现。Robolectric 自带数百个 Shadow;你也可以通过 @Implements/@Implementation 编写自己的 Shadow。常见的有:

Shadow用途
ShadowApplication检查已启动的服务、广播和已授予的权限。
ShadowLog捕获 android.util.Log 输出(ShadowLog.stream = System.out)。
ShadowSystemClock推进/冻结 SystemClock.elapsedRealtime() / uptimeMillis()。
ShadowLooper驱动消息队列;idleMainLooper()、runUiThreadTasksIncludingDelayedTasks()。
ShadowPackageManager添加/移除软件包、设置系统功能、查询可用的 intent。
ShadowAlarmManager检查已安排的闹钟而不触发它们。
ShadowNotificationManager检查已发布的通知和通知渠道。
ShadowInputManager为输入测试添加/移除 InputDevice。
ShadowContentResolver注册伪造的 provider,观察插入和查询操作。

Shadows.shadowOf(realObject) 会返回 Shadow 实例。使用静态导入,并依赖重载解析:

import androidx.test.core.app.ApplicationProvider
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowPackageManager

val app: Application = ApplicationProvider.getApplicationContext()
val shadowPm: ShadowPackageManager = shadowOf(app.packageManager)
shadowPm.setSystemFeature(PackageManager.FEATURE_CAMERA_ANY, true)

Looper 排空——最容易遗漏的步骤

在默认的 LooperMode.PAUSED 运行模式下,Robolectric 的主 looper 处于暂停状态。SUT 发布的 Handler 消息不会自动运行。请显式排空:

import android.os.Looper
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowLooper
import java.time.Duration

// Drain everything currently queued on the main looper.
shadowOf(Looper.getMainLooper()).idle()

// Run delayed tasks too (postDelayed et al).
ShadowLooper.runUiThreadTasksIncludingDelayedTasks()

// Advance virtual time by N ms; only handlers due before that time fire.
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(500))

(R7。)

这与 Compose 的 MainTestClock.advanceTimeBy(...) 相辅相成,且彼此独立。Compose 时钟和平台 looper 是分开的。对于 Robolectric 上的 Compose,runComposeUiTest { … waitForIdle() } 会通过 RobolectricIdlingStrategy 为你驱动 looper;在 Compose 之外,请手动排空。

sharedTest / androidHostTest / androidDeviceTest

经典的 sharedTest 模式(见 2021 年的 Robolectric 博文)会将一个源目录同时放入 test 和 androidTest 下:

// LEGACY — broken on AGP 7.2+
android {
    sourceSets {
        String dir = 'src/sharedTest/'
        test       { java.srcDirs += dir + 'java' }
        androidTest { java.srcDirs += dir + 'java' }
    }
}

重要:从 AGP 7.2+ 开始,不能将同一个源目录放入多个 source set(Robolectric GitHub issue #7432;R7)。构建会报错:"Source directory ... already added to source set"。

现代替代方案(按优先顺序排列):

  1. AndroidX 风格的 KMP source set——通过 androidXMultiplatform 插件,将 commonTest -> {androidHostTest, androidDeviceTest}。Compose 已在内部迁移到此模式:
    sourceSets {
        androidCommonTest  { dependsOn(commonTest) }
        androidDeviceTest  { dependsOn(androidCommonTest) }   // instrumented APK
        androidHostTest    { dependsOn(androidCommonTest) }   // Robolectric on JVM
    }
    
  2. 单独的 Gradle 模块,让 test 和 androidTest 都以 testImplementation / androidTestImplementation 依赖它。
  3. 在配置时使用符号链接或复制源文件(最不推荐——不够稳健)。

androidHostTest 和 androidDeviceTest 都可以原样使用 @RunWith(AndroidJUnit4::class),因为 AndroidJUnit4 是一个路由器。source set 的配置见 ../../../fundamentals/strategies/organizing-test-source-sets/SKILL.md。

何时不应使用 Robolectric

场景适用位置
纯 Kotlin,不导入 android.*在 src/test/ 中使用普通 JUnit4(不使用 Robolectric)。
像素级准确的渲染、截图测试模拟器/设备——RenderThread 在那里是真实的,在 Robolectric 上则不是。
无障碍服务行为模拟器/设备。Compose 的 enableAccessibilityChecks 会在 Robolectric 上安装验证器,并记录警告(Build.FINGERPRINT.lowercase() == "robolectric");Robolectric 无法忠实驱动无障碍服务,因此任何测试通过都不能作为定论(b/332778271)。
Choreographer 保真度、接近生产环境的动画时序模拟器/设备。
GPU/Skia 行为模拟器/设备。选择启用 @GraphicsMode(GraphicsMode.Mode.NATIVE) 也只是对真实管线的存根。

模式

模式:错误做法——对也应在设备上运行的测试使用 RobolectricTestRunner
// WRONG
@RunWith(RobolectricTestRunner::class)
@Config(minSdk = 23)
class LoginViewModelTest {
    @Test fun loginFlow() { /* ... */ }
}
// WRONG because: this class CAN run unchanged on a device too — there are no
// Robolectric-only APIs in the body. Pinning to RobolectricTestRunner locks the
// test to the JVM and forfeits the sharedTest pattern. The same class moved to
// src/androidTest/ (or androidDeviceTest) would not run.
// RIGHT
@RunWith(AndroidJUnit4::class)
@Config(minSdk = RobolectricMinSdk)                      // pin via single source of truth
class LoginViewModelTest {
    @Test fun loginFlow() { /* ... */ }
}
模式:错误做法——排空 looper 前就断言
// WRONG
@RunWith(AndroidJUnit4::class)
class HandlerTest {
    @Test fun postedRuns() {
        var fired = false
        Handler(Looper.getMainLooper()).post { fired = true }
        assertTrue(fired)                                 // FAIL: runnable is queued, not run
    }
}
// WRONG because: Robolectric's default LooperMode.PAUSED does not auto-run posted
// runnables. The post is queued; assertTrue runs before the queue is drained.
// RIGHT
@RunWith(AndroidJUnit4::class)
class HandlerTest {
    @Test fun postedRuns() {
        var fired = false
        Handler(Looper.getMainLooper()).post { fired = true }
        shadowOf(Looper.getMainLooper()).idle()           // drain queue
        assertTrue(fired)
    }
}
模式:错误做法——缺少 includeAndroidResources
// WRONG
android {
    testOptions {
        unitTests { /* includeAndroidResources missing */ }
    }
}

症状:测试启动时出现 android.content.res.Resources$NotFoundException: String resource ID #0x7f0c0001。修复方法:

// RIGHT
android {
    testOptions {
        unitTests {
            includeAndroidResources = true                // Groovy
            // isIncludeAndroidResources = true           // Kotlin DSL
        }
    }
}
模式:错误做法——在 AGP 7.2+ 上共享 sharedTest srcDir
// WRONG
android {
    sourceSets {
        test       { java.srcDirs += 'src/sharedTest/java' }
        androidTest { java.srcDirs += 'src/sharedTest/java' }
    }
}
// WRONG because: AGP 7.2+ rejects sharing a single source directory across
// source sets (Robolectric issue #7432). Build fails with "Source directory
// already added to source set".
// RIGHT — AndroidX-style KMP source sets
sourceSets {
    androidCommonTest  { dependsOn(commonTest) }
    androidDeviceTest  { dependsOn(androidCommonTest) }
    androidHostTest    { dependsOn(androidCommonTest) }
}
模式:ParameterizedRobolectricTestRunner——适用于测试矩阵本身就是目的的场景
// androidx/compose/ui/ui-test/.../ViewVisibilityRobolectricTest.kt (paraphrased)
@RunWith(ParameterizedRobolectricTestRunner::class)
@Config(minSdk = RobolectricMinSdk)
class ViewVisibilityRobolectricTest(private val visibility: Int) {
    companion object {
        @JvmStatic
        @Parameters(name = "visibility={0}")
        fun params() = listOf(View.VISIBLE, View.INVISIBLE, View.GONE)
    }
    @Test fun behaves() { /* uses `visibility` */ }
}

这是唯一适合指定 RobolectricTestRunner(及其参数化子类)的场景——没有 AndroidJUnit4 参数化变体。

模式:生产代码根据 Robolectric 分支——请谨慎看待
// androidx/compose/ui/ui-test/.../RobolectricIdlingStrategy.android.kt
internal val HasRobolectricFingerprint
    get() = Build.FINGERPRINT.lowercase() == "robolectric"

这种惯用写法在 androidx 内部很常见(Compose 无障碍、空闲状态、图形图层)。被测库代码 SHOULD 避免照搬,除非该分支确实仅用于测试——误判会悄无声息地改变行为。

强制规则

  • MUST 从 androidx.test.ext.junit.runners 导入 AndroidJUnit4。MUST NOT 从 androidx.test.runner 导入——后者已标记为 @Deprecated(docs/CORPUS.md §G.2)。
  • MUST 优先使用 @RunWith(AndroidJUnit4::class),而不是 @RunWith(RobolectricTestRunner::class)。仅当测试直接使用 Robolectric 专用 API(例如 shadowOf(Looper.getMainLooper()).idle())或需要 ParameterizedRobolectricTestRunner 时,才使用后者。
  • MUST 对任何单元测试会涉及 R.* / Resources / 布局 / 主题的项目,设置 testOptions.unitTests.includeAndroidResources = true。
  • MUST 在断言任何依赖 Handler.post* 回调的内容前,使用 shadowOf(Looper.getMainLooper()).idle()(或 ShadowLooper.runUiThreadTasksIncludingDelayedTasks())排空 looper。默认的 LooperMode.PAUSED 不会自动运行任务。
  • MUST 在类级别固定 @Config(minSdk = …)(或 sdk = […])——不要依赖项目默认 SDK 在 CI 中变化。
  • MUST NOT 在 AGP 7.2+ 上使用旧式 srcDirs 共享模式 test.java.srcDirs += 'src/sharedTest/java'。迁移到 androidHostTest + androidDeviceTest(KMP 源集),或使用单独的 Gradle 模块。
  • MUST NOT 在不含任何 android.* 导入的测试类中添加 Robolectric。类加载成本会白白增加 1-3 s。
  • MUST NOT 使用 Robolectric 进行截图测试、涟漪动画、无障碍服务或 RenderThread 时序测试——请使用模拟器/设备。
  • PREFERRED: 在 androidHostTest 模块中,将 RobolectricMinSdk = 23 引用自 androidx/compose/ui/ui-test/src/androidHostTest/.../Constants.kt,作为最低 SDK 的唯一真实来源。
  • PREFERRED: 将相关测试归入同一个类,以分摊每个类的类加载成本。避免在许多小类中零散添加 @RunWith(RobolectricTestRunner::class)。
  • PREFERRED: 在提交门禁上使用单 SDK 的 @Config(sdk = [33]);在合并门禁上使用多 SDK 矩阵。

验证

  • 不导入 androidx.test.runner.AndroidJUnit4(请使用 androidx.test.ext.junit.runners.AndroidJUnit4)。运行 grep -rn 'androidx.test.runner.AndroidJUnit4' src/test src/androidTest 后应无输出。
  • 模块的 build.gradle(.kts) 中已设置 testOptions.unitTests.includeAndroidResources = true。
  • 未在实际上不使用 Robolectric 专用 API 的测试类上添加 @RunWith(RobolectricTestRunner::class)。
  • 每个向 Handler 发送消息或触发 Looper 工作的测试,都在断言前调用 shadowOf(Looper.getMainLooper()).idle()(或 runUiThreadTasksIncludingDelayedTasks)。
  • 每个 Robolectric 测试类都声明了 @Config(minSdk = …) 或 @Config(sdk = […])。
  • 源集配置使用 androidHostTest + androidDeviceTest(或共享模块),而非旧式的 srcDirs += sharedTest。
  • Robolectric 下不运行截图、波纹或无障碍服务测试——这些测试应放在 src/androidTest/ 或 androidDeviceTest 中。
  • ./gradlew :module:test 通过;对于 androidCommonTest 中的测试,./gradlew :module:connectedDebugAndroidTest 也通过。

参考资料

  • Robolectric — 入门指南:http://robolectric.org/getting-started/
  • Robolectric — 配置:http://robolectric.org/configuring/
  • Robolectric — sharedTest 博客文章:http://robolectric.org/blog/2021/10/06/sharedTest/
  • Android Developers — 构建本地单元测试:https://developer.android.com/training/testing/local-tests
  • Android Developers — Robolectric:https://developer.android.com/training/testing/local-tests/robolectric
  • Robolectric GitHub issue #7432 — AGP 7.2 sharedTest 故障:https://github.com/robolectric/robolectric/issues/7432
  • 研究资料:tasks/research/R7-robolectric.md。
  • docs/CORPUS.md §G.1 / §G.6 — Gradle 坐标和 runner 选择。
  • androidx/compose/ui/ui-test/src/androidHostTest/.../Constants.kt — RobolectricMinSdk = 23 下限。
  • androidx/compose/ui/ui-test/.../RobolectricComposeTest.kt — 跨运行时测试的规范示例:@RunWith(AndroidJUnit4::class) @Config(minSdk = RobolectricMinSdk)。
  • androidx/compose/ui/ui-test/.../PrefetchNotHangingMainThreadTest.kt — 使用 @RunWith(RobolectricTestRunner::class),并显式调用 shadowOf(Looper.getMainLooper()).idle()。
  • androidx/compose/ui/ui-test/.../ViewVisibilityRobolectricTest.kt — ParameterizedRobolectricTestRunner 示例。
  • androidx/compose/ui/ui-test/.../RobolectricIdlingStrategy.android.kt — Build.FINGERPRINT.lowercase() == "robolectric" 检测惯用写法。
  • androidx/compose/ui/ui/.../MediaQueryIntegrationTest.kt — 在 @RunWith(AndroidJUnit4::class) 测试中使用 ShadowPackageManager / ShadowInputManager。
  • 同类资料:../../coroutines/testing-coroutines-with-runtest/SKILL.md — runTest + MainDispatcherRule(独立于 Robolectric)。
  • 同类资料:../../coroutines/testing-flows-with-turbine/SKILL.md — Flow 断言;Turbine 在 Robolectric 下无需修改即可使用。
  • 同类资料:../../runner/configuring-junit4-on-android/SKILL.md — Robolectric 周边的 JUnit4 配置。
  • 同类资料:../../mocking/mocking-with-mockito/SKILL.md — 在 Robolectric 下模拟 Android 类型。
  • 同类资料:../../mocking/mocking-with-mockk/SKILL.md — 针对 fingerprint 分支使用 mockkStatic(Build::class)。
  • 跨集合资料:../../../fundamentals/strategies/applying-testing-strategies/SKILL.md — 在 Google 的三级测试规模划分中,Robolectric 属于“中型”测试。
  • 跨集合资料:../../../fundamentals/strategies/organizing-test-source-sets/SKILL.md — src/test/、src/androidTest/ 与 androidHostTest / androidDeviceTest。
  • 跨集合资料:../../../compose/synchronization/controlling-the-test-clock/SKILL.md — Compose 的 MainTestClock 与 ShadowLooper 相互独立。
  • 跨集合资料:../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md — 同一测试源需要在设备上运行时的做法。

评论

支持 Markdown