Configuring Junit4 On Android
Communityskydoves
Use this skill to stand up a JUnit4-based JVM unit-test suite on an Android module. Covers the canonical Gradle dependency matrix (`junit:junit:4.13.2`, `androidx.test:core:1.7.0`, `androidx.test.ext:junit:1.3.0`, `androidx.test.ext:truth:1.7.0`), the `@RunWith(AndroidJUnit4::class)` runner, `InstrumentationRegistry.getInstrumentation()`, `ApplicationProvider.getApplicationContext()`, the `@SmallTest` / `@MediumTest` / `@LargeTest` size annotations wired into `am instrument -e size`, the androidx-test Truth subjects, and the canonical `MainDispatcherRule` setup pattern. Use when the user mentions `AndroidJUnit4 unresolved`, `which AndroidJUnit4`, `InstrumentationRegistry deprecated`, `getApplicationContext in unit test`, `Truth IntentSubject`, `@SmallTest`, `MainDispatcherRule`, or asks how to wire JUnit4 into an Android module's `src/test/`.
Overview
- Repository
- skydoves/android-testing-skills
- Publisher
- skydoves
- Stars
- 347
- Forks
- 14
- Bundled files
- 0
- License
- Apache-2.0
- Updated
- May 25, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/skydoves/android-testing-skills.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/jvm-tests/runner/configuring-junit4-on-android" ~/.claude/skills/configuring-junit4-on-android
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/skydoves/android-testing-skills.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/jvm-tests/runner/configuring-junit4-on-android" ~/.codex/skills/configuring-junit4-on-android
rm -rf "$tmp"Install the Configuring Junit4 On Android agent skill from https://github.com/skydoves/android-testing-skills/tree/main/jvm-tests/runner/configuring-junit4-on-android into this project's skills folder.Instructions
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 "whichAndroidJUnit4should I import". - The user reports
IllegalStateException: No instrumentation registeredfromInstrumentationRegistry.getInstrumentation()in a unit test, or asks whyInstrumentationRegistryis 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 smallto filter tests and asks how to wire@SmallTest/@MediumTest/@LargeTest. - The user is writing a
ViewModel/ coroutine test and needs the canonicalMainDispatcherRuletemplate.
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 { … }orTurbine— use../../coroutines/testing-coroutines-with-runtest/SKILL.mdor../../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.applicationorcom.android.library). - A
src/test/source set on disk (Gradle creates it lazily —mkdir -p src/test/kotlinif 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.xalready 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 notlatest.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 namedAndroidJUnit4on the classpath. The deprecated one isandroidx.test.runner.AndroidJUnit4(filerunner-1.7.0/androidx/test/runner/AndroidJUnit4.javaL41-44 —@Deprecated). The canonical one isandroidx.test.ext.junit.runners.AndroidJUnit4from theandroidx.test.ext:junit:1.3.0artifact (filejunit-1.3.0/androidx/test/ext/junit/runners/AndroidJUnit4.javaL49). It delegates to Robolectric on the JVM (whenorg.robolectric.RobolectricTestRunneris on the classpath) and toandroidx.test.internal.runner.junit4.AndroidJUnit4ClassRunneron 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@Deprecatedsince:monitor1.x — every member is@InlineMe'd to its replacement. The canonical class isandroidx.test.platform.app.InstrumentationRegistry(filemonitor-1.8.0/androidx/test/platform/app/InstrumentationRegistry.javaL30-85).
import androidx.test.platform.app.InstrumentationRegistry
val instrumentation = InstrumentationRegistry.getInstrumentation()
val args: Bundle = InstrumentationRegistry.getArguments() // copy of -e key/value pairsgetInstrumentation() 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 (filecore-1.7.0/androidx/test/core/app/ApplicationProvider.javaL29-43). Internally it returnsgetInstrumentation().getTargetContext().getApplicationContext()— same Context, more readable callsite, and the unchecked generic lets callers cast to theirApplicationsubclass.
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 inandroidx.test.filters.*(filerunner-1.7.0/androidx/test/filters/SmallTest.javaL44-46). The runner'sRunnerArgs.SIZE(runner-1.7.0/androidx/test/internal/runner/RunnerArgs.javaL58) 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:truthfor Android-domain assertions. Truth shipped withandroidx.test.ext:truth:1.7.0adds Android-awareSubjecttypes. Each exposes astatic assertThat(T actual)shortcut and astatic Subject.Factory<T, S> name()factory:
| Subject | FQCN package | Asserts on |
|---|---|---|
IntentSubject |
androidx.test.ext.truth.content |
android.content.Intent (action, data, component, extras, categories, flags) |
BundleSubject / BaseBundleSubject / PersistableBundleSubject |
androidx.test.ext.truth.os |
Bundle / BaseBundle / PersistableBundle keys, types, values |
ParcelableSubject |
androidx.test.ext.truth.os |
Parcelable (round-trips through a Parcel to verify the writer/reader) |
LocationSubject |
androidx.test.ext.truth.location |
android.location.Location |
NotificationSubject / NotificationActionSubject / PendingIntentSubject |
androidx.test.ext.truth.app |
Notification family |
MotionEventSubject / PointerCoordsSubject / PointerPropertiesSubject |
androidx.test.ext.truth.view |
MotionEvent family |
SparseBooleanArraySubject |
androidx.test.ext.truth.util |
SparseBooleanArray |
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
MainDispatcherRulefor any test that touchesDispatchers.Main. Compose,viewModelScope, andLiveDataall default toDispatchers.Main, which is unavailable on the JVM unless swapped. The canonical androidx pattern lives attestutils/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: AndroidJUnit4confirms the canonical runner is bound;Test process exited with status codenear 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.AndroidJUnitRunnerFor Robolectric-on-JVM tests, Gradle property filtering is the standard route:
./gradlew :app:testDebugUnitTest --tests "com.example.*" \
-PandroidTestSize=smallCombined with -e annotation com.example.SmokeTest (the RunnerArgs.ANNOTATION arg, L60) you get composable filtering.
Mandatory rules
- MUST import
AndroidJUnit4fromandroidx.test.ext.junit.runners. MUST NOT importandroidx.test.runner.AndroidJUnit4— it is@Deprecated. - MUST import
InstrumentationRegistryfromandroidx.test.platform.app. MUST NOT importandroidx.test.InstrumentationRegistry— it is@Deprecated. - MUST use
ApplicationProvider.getApplicationContext()for the Application context. MUST NOT callInstrumentationRegistry.getInstrumentation().getTargetContext()directly —ApplicationProvideris the canonical entry point and handles the unchecked cast. - MUST install a
MainDispatcherRule(or equivalentDispatchers.setMain/resetMainplumbing) before any test that touchesviewModelScope,LaunchedEffect, or anyDispatchers.Mainconsumer. Without it, the test crashes withIllegalStateException: Module with the Main dispatcher is missing. - MUST prefer
StandardTestDispatcheroverUnconfinedTestDispatcherinMainDispatcherRule.StandardTestDispatchermatchesrunTestsemantics; 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.xmatching thecompileSdkof the module. - MUST NOT use
runBlockingTest { }in new code — it is@Deprecated(level = DeprecationLevel.ERROR)sincekotlinx-coroutines-test1.7. UserunTest { }instead. - MUST NOT use
ActivityTestRule<A>— it is@Deprecated. Useandroidx.test.ext.junit.rules.ActivityScenarioRule<A>instead (junit-1.3.0/androidx/test/ext/junit/rules/ActivityScenarioRule.javaL56). For instrumented variants, see../../../instrumentation/runner/running-instrumented-tests-with-androidjunit4/SKILL.md. - PREFERRED: declare
testOptions.unitTests.isIncludeAndroidResources = truein the module'sandroid { … }block. Robolectric needs merged resources to inflate any layout or readR.string.*.
Verification
-
./gradlew :<module>:testDebugUnitTestcompiles and reportsTests run: N, Failures: 0for the new suite. -
grep -r "import androidx.test.runner.AndroidJUnit4" src/test src/androidTestreturns NO matches (onlyandroidx.test.ext.junit.runners.AndroidJUnit4is allowed). -
grep -r "import androidx.test.InstrumentationRegistry" src/test src/androidTestreturns NO matches. - Any test that uses
viewModelScope/Dispatchers.Maindeclares@get:Rule val mainDispatcherRule = MainDispatcherRule()and usesrunTest { … }. - At least one test class is annotated with
@SmallTest,@MediumTest, or@LargeTestif the suite is meant to be filtered. -
./gradlew :<module>:testDebugUnitTest --infooutput showsorg.robolectric.RobolectricTestRunner(host) orAndroidJUnit4ClassRunner(device) in the runner stack — confirming theAndroidJUnit4delegate 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) —@Deprecatedalias.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) —@Deprecatedlegacy 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, includingsize,annotation,class,package,numShards/shardIndex.testutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/MainDispatcherRule.jvm.kt— canonical androidxMainDispatcherRulesource, identical pattern shipped here.- Cross-set:
../../../kotlin/kotlin-test/writing-tests-with-kotlin-test/SKILL.md— the framework-agnostickotlin.testassertions (assertEquals,assertFailsWith,@BeforeTest) used inside these JUnit4 tests; also the only assertion API available incommonTest.
FAQ
What does Configuring Junit4 On Android do?
Use this skill to stand up a JUnit4-based JVM unit-test suite on an Android module. Covers the canonical Gradle dependency matrix (`junit:junit:4.13.2`, `androidx.test:core:1.7.0`, `androidx.test.ext:junit:1.3.0`, `androidx.test.ext:truth:1.7.0`), the `@RunWith(AndroidJUnit4::class)` runner, `InstrumentationRegistry.getInstrumentation()`, `ApplicationProvider.getApplicationContext()`, the `@SmallTest` / `@MediumTest` / `@LargeTest` size annotations wired into `am instrument -e size`, the androidx-test Truth subjects, and the canonical `MainDispatcherRule` setup pattern. Use when the user mentions `AndroidJUnit4 unresolved`, `which AndroidJUnit4`, `InstrumentationRegistry deprecated`, `getApplicationContext in unit test`, `Truth IntentSubject`, `@SmallTest`, `MainDispatcherRule`, or asks how to wire JUnit4 into an Android module's `src/test/`.
Who made Configuring Junit4 On Android?
skydoves (a community member) publishes it in the skydoves/android-testing-skills repository on GitHub.
Is Configuring Junit4 On Android free?
Yes. Configuring Junit4 On Android is open source, published by skydoves under the Apache-2.0 license.
What comes with Configuring Junit4 On Android?
Just its instructions. Everything the agent needs is in one SKILL.md file.
Which agents can use Configuring Junit4 On Android?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.