mcpField
C
configuring-junit4-on-android

configuring-junit4-on-android

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

关于 configuring-junit4-on-android

面向 Android Kotlin 测试开发者的 JUnit4 配置指南,整理 AndroidJUnit4、InstrumentationRegistry、Context、测试规模标注、Truth 断言及主调度器规则的用法。

主要功能

  1. 01选择规范的 AndroidJUnit4 Runner
  2. 02使用规范的 InstrumentationRegistry 与 ApplicationProvider
  3. 03按规模标注测试并筛选运行
  4. 04通过 Truth 断言 Android 对象
  5. 05为 Dispatchers.Main 配置 MainDispatcherRule

适用场景

  1. 01修复 AndroidJUnit4 或 InstrumentationRegistry 导入问题
  2. 02配置依赖仪器测试参数的测试
  3. 03按 small、medium、large 筛选测试
  4. 04测试使用 Dispatchers.Main 的 ViewModel 或协程

安装

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

$npx skills add https://github.com/skydoves/android-testing-skills --skill configuring-junit4-on-android

安装后在 Claude Code 中输入/configuring-junit4-on-android确认已加载。

详细介绍

在 Android 上配置 JUnit4——正确的 Runner、Registry 和依赖矩阵

几乎所有“测试套件配置错误”的问题,都可归结为三个根本原因之一:导入了错误的 AndroidJUnit4、导入了错误的 InstrumentationRegistry,或者缺少 size 注解 / dispatcher 配置。本技能整理了从 androidx.test:core 1.7.0 起的精确 Gradle 依赖矩阵、规范的 FQCN,以及 androidx 自己在 testutils-ktx 中提供的最小 MainDispatcherRule 模板。

何时使用此技能

  • 用户报告 Cannot resolve symbol AndroidJUnit4、AndroidJUnit4 is deprecated,或询问“应该导入哪个 AndroidJUnit4”。
  • 用户在单元测试中调用 InstrumentationRegistry.getInstrumentation() 时遇到 IllegalStateException: No instrumentation registered,或询问为什么 InstrumentationRegistry 被划掉了。
  • 用户需要 Truth subject(IntentSubject、BundleSubject、LocationSubject、ParcelableSubject),但不知道它们由哪个 artifact 提供。
  • 用户希望使用 am instrument -e size small 筛选测试,并询问如何配置 @SmallTest / @MediumTest / @LargeTest。
  • 用户正在编写 ViewModel / 协程测试,需要规范的 MainDispatcherRule 模板。

何时不使用此技能

  • 用户正在配置 Mockito mock——请使用 ../../mocking/mocking-with-mockito/SKILL.md。
  • 用户正在配置 MockK mock——请使用 ../../mocking/mocking-with-mockk/SKILL.md。
  • 用户正在配置 runTest { … } 或 Turbine——请使用 ../../coroutines/testing-coroutines-with-runtest/SKILL.md 或 ../../coroutines/testing-flows-with-turbine/SKILL.md。
  • 用户正在专门配置 Robolectric(SDK 矩阵、资源、@Config)——请使用 ../../robolectric/using-robolectric-correctly/SKILL.md。
  • 用户正在设备上使用 am instrument 运行测试——请使用 ../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md。
  • 用户正在选择 fake 还是 mock——请使用 ../../../fundamentals/doubles/picking-test-doubles/SKILL.md。

前置条件

  • 已应用 Android Gradle Plugin(com.android.application 或 com.android.library)。
  • 磁盘上有 src/test/ source set(Gradle 会按需创建;如果不存在,请运行 mkdir -p src/test/kotlin)。
  • Kotlin 模块(本文假设使用 Kotlin;相同的 FQCN 也适用于 Java)。
  • 对于协程相关测试,classpath 中已包含 org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.x。

工作流

  • 1. 固定规范的 Gradle 依赖矩阵。 以下是本技能采用的已发布稳定版本(已根据 ~/.gradle/caches/modules-2/files-2.1/androidx.test/... 验证)。请直接固定这些版本,不要使用 latest.release:
dependencies {
    // Core JUnit4
    testImplementation("junit:junit:4.13.2")

    // androidx.test foundations (work in src/test/ via Robolectric and in src/androidTest/)
    testImplementation("androidx.test:core:1.7.0")
    testImplementation("androidx.test:core-ktx:1.6.1")
    testImplementation("androidx.test:runner:1.7.0")
    testImplementation("androidx.test:rules:1.7.0")
    testImplementation("androidx.test.ext:junit:1.3.0")
    testImplementation("androidx.test.ext:junit-ktx:1.3.0")
    testImplementation("androidx.test.ext:truth:1.7.0")
    testImplementation("com.google.truth:truth:1.4.4")

    // Coroutines test (for MainDispatcherRule / runTest)
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
}

对于 src/androidTest/ 中的仪器化测试,请在 androidTestImplementation 上配置相同的 artifacts。这些 artifacts 兼容双 classpath。

  • 2. 使用正确的 @RunWith 注解标记测试类。 classpath 上有两个名为 AndroidJUnit4 的类。已弃用的类是 androidx.test.runner.AndroidJUnit4(文件 runner-1.7.0/androidx/test/runner/AndroidJUnit4.java L41-44——标记为 @Deprecated)。规范的类是 androidx.test.ext:junit:1.3.0 artifact 中的 androidx.test.ext.junit.runners.AndroidJUnit4(文件 junit-1.3.0/androidx/test/ext/junit/runners/AndroidJUnit4.java L49)。它在 JVM 上会委托给 Robolectric(当 classpath 中存在 org.robolectric.RobolectricTestRunner 时),在设备上则会委托给 androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner。
import androidx.test.ext.junit.runners.AndroidJUnit4   // canonical
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MyTest { /* ... */ }
  • 3. 使用规范的 InstrumentationRegistry。 androidx.test.InstrumentationRegistry(没有 .platform.app.)从 :monitor 1.x 起已标记为 @Deprecated——其所有成员都通过 @InlineMe 指向替代项。规范的类是 androidx.test.platform.app.InstrumentationRegistry(文件 monitor-1.8.0/androidx/test/platform/app/InstrumentationRegistry.java L30-85)。
import androidx.test.platform.app.InstrumentationRegistry

val instrumentation = InstrumentationRegistry.getInstrumentation()
val args: Bundle = InstrumentationRegistry.getArguments()  // copy of -e key/value pairs

getInstrumentation() 若在 instrumentation 宿主之外调用,或在 Robolectric 初始化其 shadow 之前调用,会抛出 IllegalStateException("No instrumentation registered! Must run under a registering instrumentation.")。Robolectric 会在测试运行前将一个 Instrumentation 实例注册到 registry 中,因此在 src/test/ 中启用 @RunWith(AndroidJUnit4::class) 时,此方法可以正常工作。

  • 4. 通过 ApplicationProvider 获取 Application Context,而非通过 registry。 ApplicationProvider.getApplicationContext() 是获取 Application Context 的唯一入口(文件 core-1.7.0/androidx/test/core/app/ApplicationProvider.java L29-43)。其内部返回 getInstrumentation().getTargetContext().getApplicationContext()——Context 相同,调用处更易读,而且未检查的泛型允许调用方将其转换为自己的 Application 子类。
import androidx.test.core.app.ApplicationProvider

val ctx: Context = ApplicationProvider.getApplicationContext()
val app: MyApp = ApplicationProvider.getApplicationContext()   // unchecked cast
  • 5. 当测试套件规模大到需要筛选时,为每个测试添加大小注解。 大小分为三种:@SmallTest(<200ms,不使用 Android 桩)、@MediumTest(<1000ms,通过 Robolectric 使用 Android 框架)、@LargeTest(>1000ms,经过 instrumentation)。这些注解位于 androidx.test.filters.*(文件 runner-1.7.0/androidx/test/filters/SmallTest.java L44-46)。Runner 的 RunnerArgs.SIZE(runner-1.7.0/androidx/test/internal/runner/RunnerArgs.java L58)会将 -e size <value> 直接映射到这些注解。
import androidx.test.filters.SmallTest
import androidx.test.filters.MediumTest

@SmallTest
class FastValidatorTest { /* ... */ }

@MediumTest
class RoomDaoTest { /* ... */ }

相应的 am instrument 调用为 adb shell am instrument -w -r -e size small <pkg>/androidx.test.runner.AndroidJUnitRunner。设备端的对应内容请参阅 ../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md。

  • 6. 对 Android 领域的断言,优先使用 androidx.test.ext:truth 中的 Truth subject。 Truth 随 androidx.test.ext:truth:1.7.0 一同发布,增加了面向 Android 的 Subject 类型。每种类型都提供 static assertThat(T actual) 快捷方法和 static Subject.Factory<T, S> name() 工厂方法:
Subject完全限定类名包断言对象
IntentSubjectandroidx.test.ext.truth.contentandroid.content.Intent(action、data、component、extras、categories、flags)
BundleSubject / BaseBundleSubject / PersistableBundleSubjectandroidx.test.ext.truth.osBundle / BaseBundle / PersistableBundle 的键、类型和值
ParcelableSubjectandroidx.test.ext.truth.osParcelable(通过 Parcel 往返传递,以验证写入器/读取器)
LocationSubjectandroidx.test.ext.truth.locationandroid.location.Location
NotificationSubject / NotificationActionSubject / PendingIntentSubjectandroidx.test.ext.truth.appNotification 系列
MotionEventSubject / PointerCoordsSubject / PointerPropertiesSubjectandroidx.test.ext.truth.viewMotionEvent 系列
SparseBooleanArraySubjectandroidx.test.ext.truth.utilSparseBooleanArray

惯用的导入和调用方式:

import androidx.test.ext.truth.content.IntentSubject.assertThat
import androidx.test.ext.truth.os.BundleSubject

@Test fun intent_routes_to_settings() {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse("settings://wifi"))
    assertThat(intent).hasAction(Intent.ACTION_VIEW)
    assertThat(intent).hasData(Uri.parse("settings://wifi"))
}
  • 7. 对任何使用 Dispatchers.Main 的测试,安装 MainDispatcherRule。 Compose、viewModelScope 和 LiveData 默认都使用 Dispatchers.Main,除非替换它,否则在 JVM 上不可用。androidx 的规范模式位于 testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/MainDispatcherRule.jvm.kt——请原样使用:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
    val testDispatcher: TestDispatcher = StandardTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

应用于测试类:

@RunWith(AndroidJUnit4::class)
class MyViewModelTest {
    @get:Rule val mainDispatcherRule = MainDispatcherRule()

    @Test fun loadsUsers() = runTest {
        val vm = MyViewModel(repo = FakeUserRepository())
        vm.load()
        advanceUntilIdle()
        assertThat(vm.users.value).isNotEmpty()
    }
}

首选 StandardTestDispatcher——它会将续体加入队列,并且只在调用 runCurrent()/advanceUntilIdle() 时推进,这与 kotlinx.coroutines.test.runTest 的语义一致。UnconfinedTestDispatcher 会立即分发,仅适用于执行顺序无关紧要的情况。

  • 8. 确认测试类确实会运行。 执行一次测试套件:./gradlew :<module>:testDebugUnitTest --tests com.example.MyTest。Gradle 输出会显示运行器选择情况——org.junit.runner.RunWith: AndroidJUnit4 表示已绑定规范运行器;顶部附近的 Test process exited with status code 表示 Robolectric 已完成配置。

模式

模式:错误与正确——选择 AndroidJUnit4 运行器
// WRONG
import org.junit.runners.JUnit4
import org.junit.runner.RunWith

@RunWith(JUnit4::class)
class MyTest { /* uses InstrumentationRegistry, ApplicationProvider, ... */ }
// WRONG because: stock JUnit4 does not register the Android `Instrumentation` instance
// into InstrumentationRegistry. ApplicationProvider.getApplicationContext() throws
// IllegalStateException("No instrumentation registered!"). Robolectric is also never
// activated even when on the classpath, so all framework calls hit unmocked stubs.
// RIGHT
import androidx.test.ext.junit.runners.AndroidJUnit4   // ext.junit.runners, NOT runner.AndroidJUnit4
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MyTest { /* ... */ }

已弃用的别名 androidx.test.runner.AndroidJUnit4 仍可编译,但会产生未抑制的弃用警告。新代码必须从 androidx.test.ext.junit.runners 导入。

模式:错误与正确——导入 InstrumentationRegistry
// WRONG
import androidx.test.InstrumentationRegistry        // @Deprecated since :monitor

val ctx = InstrumentationRegistry.getContext()      // also deprecated — not the Application context
// WRONG because: every member of androidx.test.InstrumentationRegistry is @Deprecated
// and @InlineMe'd to its replacement. getContext() returns the instrumentation package
// context, NOT the Application context — common source of "resource not found" failures.
// RIGHT
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.core.app.ApplicationProvider

val ctx: Context = ApplicationProvider.getApplicationContext()    // application context
val args: Bundle = InstrumentationRegistry.getArguments()          // -e key/value pairs
val instr = InstrumentationRegistry.getInstrumentation()
模式:错误与正确——在单元测试中访问 Compose Dispatchers.Main
// WRONG
class MyViewModelTest {
    @Test fun loadsUsers() = runBlocking {
        val vm = MyViewModel(repo = FakeUserRepository())
        vm.load()
        // Throws IllegalStateException: "Module with the Main dispatcher is missing"
    }
}
// WRONG because: kotlinx-coroutines-android (the Main dispatcher impl for Android) is
// NOT on the unit-test classpath. Without Dispatchers.setMain(...), any viewModelScope or
// LaunchedEffect that touches the Main dispatcher fails immediately.
// RIGHT
class MyViewModelTest {
    @get:Rule val mainDispatcherRule = MainDispatcherRule()

    @Test fun loadsUsers() = runTest {
        val vm = MyViewModel(repo = FakeUserRepository())
        vm.load()
        advanceUntilIdle()
        assertThat(vm.users.value).hasSize(2)
    }
}
模式:从命令行按大小筛选

@SmallTest / @MediumTest / @LargeTest 注解会直接绑定到 RunnerArgs.SIZE(runner-1.7.0/androidx/test/internal/runner/RunnerArgs.java L58)。对于插桩测试:

adb shell am instrument -w -r -e size small \
    com.example.test/androidx.test.runner.AndroidJUnitRunner

对于运行在 JVM 上的 Robolectric 测试,Gradle 属性筛选是标准方式:

./gradlew :app:testDebugUnitTest --tests "com.example.*" \
    -PandroidTestSize=small

结合 -e annotation com.example.SmokeTest(即 RunnerArgs.ANNOTATION 参数,L60),即可组合筛选条件。

强制规则

  • 必须从 androidx.test.ext.junit.runners 导入 AndroidJUnit4。不得导入 androidx.test.runner.AndroidJUnit4——它已标记为 @Deprecated。
  • 必须从 androidx.test.platform.app 导入 InstrumentationRegistry。不得导入 androidx.test.InstrumentationRegistry——它已标记为 @Deprecated。
  • 必须使用 ApplicationProvider.getApplicationContext() 获取 Application 上下文。不得直接调用 InstrumentationRegistry.getInstrumentation().getTargetContext()——ApplicationProvider 是规范入口,可处理未经检查的类型转换。
  • 必须在任何会触及 viewModelScope、LaunchedEffect 或任何 Dispatchers.Main 使用者的测试之前安装 MainDispatcherRule(或等效的 Dispatchers.setMain/resetMain 配置)。否则,测试会因 IllegalStateException: Module with the Main dispatcher is missing 而崩溃。
  • 必须在 MainDispatcherRule 中优先使用 StandardTestDispatcher,而不是 UnconfinedTestDispatcher。StandardTestDispatcher 与 runTest 语义一致;立即调度可能导致执行顺序出错。
  • 当测试套件按大小筛选时,必须为每个测试类或方法恰好添加一个大小注解(@SmallTest / @MediumTest / @LargeTest)。在同一目标上使用多个大小注解会导致运行器行为未定义。
  • 必须单独指定 Robolectric 版本——它不包含在 androidx-test BOM 中。使用与模块 compileSdk 匹配的 org.robolectric:robolectric:4.x。
  • 新代码中不得使用 runBlockingTest { }——自 kotlinx-coroutines-test 1.7 起,它已标记为 @Deprecated(level = DeprecationLevel.ERROR)。请改用 runTest { }。
  • 不得使用 ActivityTestRule<A>——它已标记为 @Deprecated。请改用 androidx.test.ext.junit.rules.ActivityScenarioRule<A>(junit-1.3.0/androidx/test/ext/junit/rules/ActivityScenarioRule.java L56)。有关插桩测试版本,请参阅 ../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md。
  • **首选:**在模块的 android { … } 代码块中声明 testOptions.unitTests.isIncludeAndroidResources = true。Robolectric 需要合并后的资源,才能填充任何布局或读取 R.string.*。

验证

  • ./gradlew :<module>:testDebugUnitTest 可成功编译,并为新测试套件报告 Tests run: N, Failures: 0。
  • grep -r "import androidx.test.runner.AndroidJUnit4" src/test src/androidTest 无匹配结果(仅允许使用 androidx.test.ext.junit.runners.AndroidJUnit4)。
  • grep -r "import androidx.test.InstrumentationRegistry" src/test src/androidTest 无匹配结果。
  • 任何使用 viewModelScope / Dispatchers.Main 的测试都声明 @get:Rule val mainDispatcherRule = MainDispatcherRule(),并使用 runTest { … }。
  • 如果测试套件需要筛选,至少有一个测试类使用 @SmallTest、@MediumTest 或 @LargeTest 注解。
  • ./gradlew :<module>:testDebugUnitTest --info 的输出在运行器堆栈中显示 org.robolectric.RobolectricTestRunner(主机)或 AndroidJUnit4ClassRunner(设备),以确认选择了 AndroidJUnit4 代理。

参考资料

  • AndroidX Test 版本发布记录:https://developer.android.com/jetpack/androidx/releases/test
  • 本地测试指南:https://developer.android.com/training/testing/local-tests
  • 插桩测试指南:https://developer.android.com/training/testing/instrumented-tests
  • AndroidJUnitRunner 参考:https://developer.android.com/training/testing/junit-runner
  • Truth subject 参考(上游代码树):https://github.com/android/android-test/tree/main/ext/truth/java/androidx/test/ext/truth
  • runner-1.7.0/androidx/test/runner/AndroidJUnit4.java(L41-44)——@Deprecated 别名。
  • junit-1.3.0/androidx/test/ext/junit/runners/AndroidJUnit4.java(L49-70)——规范运行器,可选择 Robolectric 或 Android 代理。
  • monitor-1.8.0/androidx/test/platform/app/InstrumentationRegistry.java(L30-85)——规范注册表;提供 getInstrumentation、getArguments、registerInstance。
  • monitor-1.8.0/androidx/test/InstrumentationRegistry.java(L34)——@Deprecated 旧版注册表。
  • core-1.7.0/androidx/test/core/app/ApplicationProvider.java(L29-43)——单方法门面,返回 Application 上下文。
  • runner-1.7.0/androidx/test/filters/SmallTest.java(L44-46)、MediumTest.java、LargeTest.java——测试规模注解定义。
  • runner-1.7.0/androidx/test/internal/runner/RunnerArgs.java(L54-99)——运行器接受的所有 -e <key> <value> 参数,包括 size、annotation、class、package、numShards/shardIndex。
  • testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/MainDispatcherRule.jvm.kt——规范的 androidx MainDispatcherRule 源码,与此处提供的模式相同。
  • 跨测试集:../../../kotlin/kotlin-test/writing-tests-with-kotlin-test/SKILL.md——这些 JUnit4 测试中使用的、与框架无关的 kotlin.test 断言(assertEquals、assertFailsWith、@BeforeTest);也是 commonTest 中唯一可用的断言 API。

评论

支持 Markdown