| name | configuring-junit4-on-android |
| description | 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/`. |
| license | Apache-2.0. See LICENSE for complete terms. |
| metadata | {"author":"Jaewoong Eum (skydoves)","keywords":["android-testing","junit4","jvm-unit-tests","androidx-test","AndroidJUnit4","InstrumentationRegistry","ApplicationProvider","SmallTest","MainDispatcherRule","test-size-annotations"]} |
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
dependencies {
testImplementation("junit:junit:4.13.2")
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")
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.
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MyTest { }
import androidx.test.platform.app.InstrumentationRegistry
val instrumentation = InstrumentationRegistry.getInstrumentation()
val args: Bundle = InstrumentationRegistry.getArguments()
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.
import androidx.test.core.app.ApplicationProvider
val ctx: Context = ApplicationProvider.getApplicationContext()
val app: MyApp = ApplicationProvider.getApplicationContext()
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.
| 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"))
}
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.
Patterns
Pattern: WRONG vs RIGHT — choosing the AndroidJUnit4 runner
import org.junit.runners.JUnit4
import org.junit.runner.RunWith
@RunWith(JUnit4::class)
class MyTest { }
import androidx.test.ext.junit.runners.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
import androidx.test.InstrumentationRegistry
val ctx = InstrumentationRegistry.getContext()
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.core.app.ApplicationProvider
val ctx: Context = ApplicationProvider.getApplicationContext()
val args: Bundle = InstrumentationRegistry.getArguments()
val instr = InstrumentationRegistry.getInstrumentation()
Pattern: WRONG vs RIGHT — accessing Compose Dispatchers.Main from a unit test
class MyViewModelTest {
@Test fun loadsUsers() = runBlocking {
val vm = MyViewModel(repo = FakeUserRepository())
vm.load()
}
}
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 . Use instead ( L56). For instrumented variants, see .
Verification
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 assertions (, , ) used inside these JUnit4 tests; also the only assertion API available in .