mcpField
C
configuring-junit4-on-android

configuring-junit4-on-android

github.com/skydovesTesting & quality33974 installsUpdated 22w ago
Repository

About configuring-junit4-on-android

A JUnit4 configuration guide for Android Kotlin test authors. It covers the canonical AndroidJUnit4 runner and registry, application context access, size annotations, Android-specific Truth assertions, and a main-dispatcher rule.

Key features

  1. 01Select the canonical AndroidJUnit4 runner
  2. 02Use InstrumentationRegistry and ApplicationProvider
  3. 03Annotate and filter tests by size
  4. 04Assert Android objects with Truth
  5. 05Configure MainDispatcherRule for Dispatchers.Main

Use cases

  1. 01Fix AndroidJUnit4 or InstrumentationRegistry import issues
  2. 02Configure tests that use instrumentation arguments
  3. 03Filter tests by small, medium, or large size
  4. 04Test a ViewModel or coroutine that uses Dispatchers.Main

Install

Copy the command and run it in a terminal.

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

After installing, type/configuring-junit4-on-androidin Claude Code to confirm it loaded.

Details

From the project README

Configuring JUnit4 on Android — The Right Runner, Registry, and Matrix

Almost every "test suite is misconfigured" report boils down to one of three root causes: the wrong AndroidJUnit4 is imported, the wrong InstrumentationRegistry is imported, or the size-annotation / dispatcher plumbing is missing. This skill encodes the exact Gradle matrix from androidx.test:core 1.7.0 onward, the canonical FQCNs, and the minimal MainDispatcherRule pattern that androidx itself ships in testutils-ktx.

When to use this skill

  • The user reports Cannot resolve symbol AndroidJUnit4, AndroidJUnit4 is deprecated, or asks "which AndroidJUnit4 should I import".
  • The user reports IllegalStateException: No instrumentation registered from InstrumentationRegistry.getInstrumentation() in a unit test, or asks why InstrumentationRegistry is crossed out.
  • The user wants Truth subjects (IntentSubject, BundleSubject, LocationSubject, ParcelableSubject) but does not know which artifact ships them.
  • The user wants am instrument -e size small to filter tests and asks how to wire @SmallTest / @MediumTest / @LargeTest.
  • The user is writing a ViewModel / coroutine test and needs the canonical MainDispatcherRule template.

When NOT to use this skill

  • The user is wiring Mockito mocks — use ../../mocking/mocking-with-mockito/SKILL.md.
  • The user is wiring MockK mocks — use ../../mocking/mocking-with-mockk/SKILL.md.
  • The user is configuring runTest { … } or Turbine — use ../../coroutines/testing-coroutines-with-runtest/SKILL.md or ../../coroutines/testing-flows-with-turbine/SKILL.md.
  • The user is configuring Robolectric specifically (SDK matrix, resources, @Config) — use ../../robolectric/using-robolectric-correctly/SKILL.md.
  • The user is running tests on a device with am instrument — use ../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md.
  • The user is choosing between fakes and mocks — use ../../../fundamentals/doubles/picking-test-doubles/SKILL.md.

Prerequisites

  • Android Gradle Plugin applied (com.android.application or com.android.library).
  • A src/test/ source set on disk (Gradle creates it lazily — mkdir -p src/test/kotlin if missing).
  • Kotlin module (these notes assume Kotlin; the same FQCNs work from Java).
  • For coroutine-related tests, org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.x already on the classpath.

Workflow

  • 1. Pin the canonical Gradle dependency matrix. These are the released stable versions backing this skill (verified against ~/.gradle/caches/modules-2/files-2.1/androidx.test/...). Pin them directly, do not 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")
}

For instrumented tests in src/androidTest/, mirror the same artifacts on androidTestImplementation. The artifacts are dual-classpath compatible.

  • 2. Annotate test classes with the correct @RunWith. There are TWO classes named AndroidJUnit4 on the classpath. The deprecated one is androidx.test.runner.AndroidJUnit4 (file runner-1.7.0/androidx/test/runner/AndroidJUnit4.java L41-44 — @Deprecated). The canonical one is androidx.test.ext.junit.runners.AndroidJUnit4 from the androidx.test.ext:junit:1.3.0 artifact (file junit-1.3.0/androidx/test/ext/junit/runners/AndroidJUnit4.java L49). It delegates to Robolectric on the JVM (when org.robolectric.RobolectricTestRunner is on the classpath) and to androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner on a device.
import androidx.test.ext.junit.runners.AndroidJUnit4   // canonical
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MyTest { /* ... */ }
  • 3. Use the canonical InstrumentationRegistry. androidx.test.InstrumentationRegistry (no .platform.app.) is @Deprecated since :monitor 1.x — every member is @InlineMe'd to its replacement. The canonical class is androidx.test.platform.app.InstrumentationRegistry (file 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() throws IllegalStateException("No instrumentation registered! Must run under a registering instrumentation.") if called outside an instrumentation host or before Robolectric initialises its shadow. Robolectric registers an Instrumentation instance into the registry before tests run, so this works in src/test/ when @RunWith(AndroidJUnit4::class) is in effect.

  • 4. Pull the application Context via ApplicationProvider, not via the registry. ApplicationProvider.getApplicationContext() is the single entry point for the Application context (file core-1.7.0/androidx/test/core/app/ApplicationProvider.java L29-43). Internally it returns getInstrumentation().getTargetContext().getApplicationContext() — same Context, more readable callsite, and the unchecked generic lets callers cast to their Application subclass.
import androidx.test.core.app.ApplicationProvider

val ctx: Context = ApplicationProvider.getApplicationContext()
val app: MyApp = ApplicationProvider.getApplicationContext()   // unchecked cast
  • 5. Tag each test with a size annotation when the suite is large enough to need filtering. Size is one of three: @SmallTest (<200ms, no Android stubs), @MediumTest (<1000ms, Android framework via Robolectric), @LargeTest (>1000ms, instrumented). Annotations live in androidx.test.filters.* (file runner-1.7.0/androidx/test/filters/SmallTest.java L44-46). The runner's RunnerArgs.SIZE (runner-1.7.0/androidx/test/internal/runner/RunnerArgs.java L58) wires -e size <value> directly to these annotations.
import androidx.test.filters.SmallTest
import androidx.test.filters.MediumTest

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

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

The corresponding am instrument invocation is adb shell am instrument -w -r -e size small <pkg>/androidx.test.runner.AndroidJUnitRunner. See ../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md for the on-device counterpart.

  • 6. Reach for Truth subjects from androidx.test.ext:truth for Android-domain assertions. Truth shipped with androidx.test.ext:truth:1.7.0 adds Android-aware Subject types. Each exposes a static assertThat(T actual) shortcut and a static Subject.Factory<T, S> name() factory:
SubjectFQCN packageAsserts on
IntentSubjectandroidx.test.ext.truth.contentandroid.content.Intent (action, data, component, extras, categories, flags)
BundleSubject / BaseBundleSubject / PersistableBundleSubjectandroidx.test.ext.truth.osBundle / BaseBundle / PersistableBundle keys, types, values
ParcelableSubjectandroidx.test.ext.truth.osParcelable (round-trips through a Parcel to verify the writer/reader)
LocationSubjectandroidx.test.ext.truth.locationandroid.location.Location
NotificationSubject / NotificationActionSubject / PendingIntentSubjectandroidx.test.ext.truth.appNotification family
MotionEventSubject / PointerCoordsSubject / PointerPropertiesSubjectandroidx.test.ext.truth.viewMotionEvent family
SparseBooleanArraySubjectandroidx.test.ext.truth.utilSparseBooleanArray

Idiomatic import-and-call:

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. Install a MainDispatcherRule for any test that touches Dispatchers.Main. Compose, viewModelScope, and LiveData all default to Dispatchers.Main, which is unavailable on the JVM unless swapped. The canonical androidx pattern lives at testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/MainDispatcherRule.jvm.kt — use it verbatim:
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()
    }
}

Apply on a test class:

@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 is preferred — it queues continuations and only advances on runCurrent()/advanceUntilIdle(), matching kotlinx.coroutines.test.runTest semantics. UnconfinedTestDispatcher dispatches eagerly and is appropriate only when ordering is irrelevant.

  • 8. Verify the test class actually runs. Execute the suite once: ./gradlew :<module>:testDebugUnitTest --tests com.example.MyTest. The Gradle output shows the runner choice — org.junit.runner.RunWith: AndroidJUnit4 confirms the canonical runner is bound; Test process exited with status code near the top confirms Robolectric is wiring up.

Patterns

Pattern: WRONG vs RIGHT — choosing the AndroidJUnit4 runner
// 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 { /* ... */ }

The deprecated alias androidx.test.runner.AndroidJUnit4 still compiles but emits an unsuppressed deprecation warning. New code MUST import from androidx.test.ext.junit.runners.

Pattern: WRONG vs RIGHT — InstrumentationRegistry import
// 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()
Pattern: WRONG vs RIGHT — accessing Compose Dispatchers.Main from a unit test
// 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)
    }
}
Pattern: filtering by size from the command line

The @SmallTest / @MediumTest / @LargeTest annotations bind directly to RunnerArgs.SIZE (runner-1.7.0/androidx/test/internal/runner/RunnerArgs.java L58). For instrumented tests:

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

For Robolectric-on-JVM tests, Gradle property filtering is the standard route:

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

Combined with -e annotation com.example.SmokeTest (the RunnerArgs.ANNOTATION arg, L60) you get composable filtering.

Mandatory rules

  • MUST import AndroidJUnit4 from androidx.test.ext.junit.runners. MUST NOT import androidx.test.runner.AndroidJUnit4 — it is @Deprecated.
  • MUST import InstrumentationRegistry from androidx.test.platform.app. MUST NOT import androidx.test.InstrumentationRegistry — it is @Deprecated.
  • MUST use ApplicationProvider.getApplicationContext() for the Application context. MUST NOT call InstrumentationRegistry.getInstrumentation().getTargetContext() directly — ApplicationProvider is the canonical entry point and handles the unchecked cast.
  • MUST install a MainDispatcherRule (or equivalent Dispatchers.setMain/resetMain plumbing) before any test that touches viewModelScope, LaunchedEffect, or any Dispatchers.Main consumer. Without it, the test crashes with IllegalStateException: Module with the Main dispatcher is missing.
  • MUST prefer StandardTestDispatcher over UnconfinedTestDispatcher in MainDispatcherRule. StandardTestDispatcher matches runTest semantics; eager dispatch is a footgun for ordering.
  • MUST annotate every test class or method with exactly ONE size annotation (@SmallTest / @MediumTest / @LargeTest) when the suite is filtered by size. Multiple size annotations on the same target produce undefined runner behaviour.
  • MUST pin Robolectric independently — it does not ship in the androidx-test BOM. Use org.robolectric:robolectric:4.x matching the compileSdk of the module.
  • MUST NOT use runBlockingTest { } in new code — it is @Deprecated(level = DeprecationLevel.ERROR) since kotlinx-coroutines-test 1.7. Use runTest { } instead.
  • MUST NOT use ActivityTestRule<A> — it is @Deprecated. Use androidx.test.ext.junit.rules.ActivityScenarioRule<A> instead (junit-1.3.0/androidx/test/ext/junit/rules/ActivityScenarioRule.java L56). For instrumented variants, see ../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md.
  • PREFERRED: declare testOptions.unitTests.isIncludeAndroidResources = true in the module's android { … } block. Robolectric needs merged resources to inflate any layout or read R.string.*.

Verification

  • ./gradlew :<module>:testDebugUnitTest compiles and reports Tests run: N, Failures: 0 for the new suite.
  • grep -r "import androidx.test.runner.AndroidJUnit4" src/test src/androidTest returns NO matches (only androidx.test.ext.junit.runners.AndroidJUnit4 is allowed).
  • grep -r "import androidx.test.InstrumentationRegistry" src/test src/androidTest returns NO matches.
  • Any test that uses viewModelScope / Dispatchers.Main declares @get:Rule val mainDispatcherRule = MainDispatcherRule() and uses runTest { … }.
  • At least one test class is annotated with @SmallTest, @MediumTest, or @LargeTest if the suite is meant to be filtered.
  • ./gradlew :<module>:testDebugUnitTest --info output shows org.robolectric.RobolectricTestRunner (host) or AndroidJUnit4ClassRunner (device) in the runner stack — confirming the AndroidJUnit4 delegate selection.

References

  • AndroidX Test releases: https://developer.android.com/jetpack/androidx/releases/test
  • Local tests guide: https://developer.android.com/training/testing/local-tests
  • Instrumented tests guide: https://developer.android.com/training/testing/instrumented-tests
  • AndroidJUnitRunner reference: https://developer.android.com/training/testing/junit-runner
  • Truth subject reference (upstream tree): 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 alias.
  • junit-1.3.0/androidx/test/ext/junit/runners/AndroidJUnit4.java (L49-70) — canonical runner with Robolectric/Android delegate selection.
  • monitor-1.8.0/androidx/test/platform/app/InstrumentationRegistry.java (L30-85) — canonical registry; getInstrumentation, getArguments, registerInstance.
  • monitor-1.8.0/androidx/test/InstrumentationRegistry.java (L34) — @Deprecated legacy registry.
  • core-1.7.0/androidx/test/core/app/ApplicationProvider.java (L29-43) — single-method facade returning the Application context.
  • runner-1.7.0/androidx/test/filters/SmallTest.java (L44-46), MediumTest.java, LargeTest.java — size annotation definitions.
  • runner-1.7.0/androidx/test/internal/runner/RunnerArgs.java (L54-99) — every -e <key> <value> argument the runner accepts, including size, annotation, class, package, numShards/shardIndex.
  • testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/MainDispatcherRule.jvm.kt — canonical androidx MainDispatcherRule source, identical pattern shipped here.
  • Cross-set: ../../../kotlin/kotlin-test/writing-tests-with-kotlin-test/SKILL.md — the framework-agnostic kotlin.test assertions (assertEquals, assertFailsWith, @BeforeTest) used inside these JUnit4 tests; also the only assertion API available in commonTest.

Comments

Markdown supported