This file is the distilled knowledge base for android-testing-skills. Every fact, API signature,
and behavioral note here is grounded in a file path inside androidx/, an authoritative external
doc, or one of the deep research reports in tasks/research/. Skill authors MUST pull facts
and citations from this corpus so vocabulary and claims stay consistent across skills.
The repository organises skills into five sets:
compose/— Jetpack Compose UI testing (22 skills). Authoritative source:androidx/compose/ui/ui-test*.fundamentals/— testing pyramid, what to test, test doubles, strategies, source sets. Authoritative source: developer.android.com/training/testing/fundamentals +tasks/research/R8-android-fundamentals.md.jvm-tests/— JVM unit tests: JUnit4, Mockito, MockK, kotlinx-coroutines-test, Turbine, Robolectric. Seetasks/research/R1,R5,R6,R7.instrumentation/— on-device tests: AndroidJUnit4, ActivityScenario, FragmentScenario, Espresso, UiAutomator. Seetasks/research/R1,R2,R3,R4.adb/— ADB-driven device E2E. Seetasks/research/A1,A2,A3.
Per-set deep research reports under tasks/research/ are the long form. This CORPUS file is the
short form — what every skill author needs at their fingertips.
Cite these directly in SKILL.md "References" sections.
- Compose testing overview: https://developer.android.com/develop/ui/compose/testing
- Compose testing setup: https://developer.android.com/develop/ui/compose/testing#setup
- Compose testing cheat sheet: https://developer.android.com/develop/ui/compose/testing-cheatsheet
- Semantics in Compose: https://developer.android.com/develop/ui/compose/accessibility/semantics
- Testing animations: https://developer.android.com/develop/ui/compose/animation/testing
- Compose Multiplatform testing: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html
androidx.compose.ui:ui-test— common test API (finders, matchers, assertions, actions,MainTestClock,IdlingResource,ComposeUiTest).androidx.compose.ui:ui-test-junit4— JUnit4 surface (ComposeTestRule,createComposeRule,StateRestorationTester).androidx.compose.ui:ui-test-manifest—debugImplementationonly; merges an<activity>entry forandroidx.activity.ComponentActivitysocreateComposeRule()can launch it.androidx.compose.ui:ui-test-accessibility—enableAccessibilityChecksforComposeUiTest(@RequiresApi(34)).androidx.compose.ui:ui-test-junit4-accessibility— same forComposeTestRule/AndroidComposeTestRule.- Release notes: https://developer.android.com/jetpack/androidx/releases/compose-ui
- Adam Powell — "How does compose-runtime test work?": https://issuetracker.google.com (search "compose ui test")
- Chris Banes — Compose testing posts: https://chrisbanes.me/tags/jetpack-compose/
- Android Developers blog — Testing posts tag: https://android-developers.googleblog.com/search/label/testing
- compose-performance-skills (sibling repo): https://github.com/skydoves/compose-performance-skills
- compose-effects: https://github.com/skydoves/compose-effects
- compose-stability-analyzer: https://github.com/skydoves/compose-stability-analyzer
Every skill MUST ground API claims in one of these files. Paths are relative to
/Users/jaewoong/Desktop/projects/study/androidx/. Subfolder shorthand ui-test-cm =
compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/.
| File | Owns |
|---|---|
ui-test-cm/Finders.kt |
onNodeWithTag/Text/ContentDescription, onAllNodesWith*, onRoot (all @CheckResult thin wrappers over SemanticsNodeInteractionsProvider.onNode/onAllNodes) |
ui-test-cm/SemanticsNodeInteractionsProvider.kt |
onNode(matcher, useUnmergedTree=false), onAllNodes(matcher, useUnmergedTree=false) |
ui-test-cm/SemanticsNodeInteraction.kt |
Lazy single-node and collection handles, assertExists, assertDoesNotExist, assertIsDeactivated, fetchSemanticsNode, collection [index] |
ui-test-cm/Selectors.kt |
onParent, onChildren, onChild, onChildAt, onSibling(s), onAncestors, onFirst, onLast, filter, filterToOne |
ui-test-cm/SemanticsMatcher.kt |
class SemanticsMatcher, expectValue/keyIsDefined/keyNotDefined factories, infix and, infix or, operator not |
ui-test-cm/Filters.kt |
Every prebuilt matcher: isEnabled, isOn, hasText, hasContentDescription, hasTestTag, hasClickAction, hasScrollAction, isRoot, isDialog, isPopup, hasParent, hasAnyAncestor, hasAnyChild, hasAnySibling, hasAnyDescendant, hasImeAction, hasScrollToIndexAction, hasScrollToKeyAction, hasScrollToNodeAction |
ui-test-cm/Assertions.kt |
Every assert* extension: assertIsDisplayed, assertIsNotDisplayed, assertIsEnabled, assertIsOn/Off, assertIsSelected, assertHasClickAction, assertTextEquals, assertTextContains, assertContentDescriptionEquals, assertCountEquals, assertAll, assertAny, assert(matcher) |
ui-test-cm/BoundsAssertions.kt |
assertWidthIsEqualTo, assertHeightIsAtLeast, assertPositionInRootIsEqualTo, assertLeftPositionInRootIsEqualTo, getUnclippedBoundsInRoot, getBoundsInRoot, getAlignmentLinePosition, Dp.assertIsEqualTo (default tolerance ½ dp) |
ui-test-cm/Actions.kt |
performClick, performScrollTo, performScrollToIndex/Key/Node, performTouchInput, performMouseInput, performKeyInput, performMultiModalInput, performTrackpadInput, performRotaryScrollInput, requestFocus, performSemanticsAction, performCustomAccessibilityActionWithLabel* (experimental), performFirstLinkClick, performIndirectPointerInput, tryPerformAccessibilityChecks. performGesture is @Deprecated |
ui-test-cm/TextActions.kt |
performTextInput, performTextReplacement, performTextClearance, performTextInputSelection, performImeAction |
ui-test-cm/KeyInputHelpers.kt |
performKeyPress(KeyEvent): Boolean (low-level, sends to focused root) |
ui-test-cm/InjectionScope.kt |
interface InjectionScope : Density. Geometry helpers: width, height, left/top/right/bottom, centerX/Y, topLeft/topCenter/topRight/centerLeft/center/centerRight/bottomLeft/bottomCenter/bottomRight, percentOffset, eventPeriodMillis, advanceEventTime, viewConfiguration, visibleSize |
ui-test-cm/TouchInjectionScope.kt |
Touch DSL: down/up/moveBy/moveTo/cancel. Extensions: click, longClick, doubleClick, swipe, swipeUp/Down/Left/Right, pinch, swipeWithVelocity, multiTouchSwipe |
ui-test-cm/MouseInjectionScope.kt |
press/release/scroll + extensions click, rightClick, doubleClick, tripleClick, longClick, animateMoveTo/By/Along, dragAndDrop, smoothScroll |
ui-test-cm/KeyInjectionScope.kt |
keyDown/keyUp/isKeyDown, modifier-state vals (isCtrlDown/isShiftDown/etc.), helpers: pressKey, withKeyDown, withKeysDown, withKeyToggled, withKeysToggled |
ui-test-cm/RotaryInjectionScope.kt |
rotateToScrollVertically, rotateToScrollHorizontally (Wear OS) |
ui-test-cm/MultiModalInjectionScope.kt |
Aggregator: touch, mouse, key, rotary, trackpad, indirectPointer |
ui-test-cm/Mouse.kt |
value class ScrollWheel { Horizontal, Vertical }, expect value class MouseButton { Primary, Secondary, Tertiary } |
ui-test-cm/Output.kt |
printToLog(tag, maxDepth), printToString(maxDepth) for both single and collection. Default depth is Int.MAX_VALUE (single) / 0 (collection) |
ui-test-cm/MainTestClock.kt |
Public clock interface, ComposeTimeoutException |
ui-test-cm/AbstractMainTestClock.kt |
Abstract impl wrapping TestCoroutineScheduler |
ui-test-cm/ComposeUiTest.kt |
expect sealed interface ComposeUiTest, free waitUntilNodeCount/AtLeastOneExists/ExactlyOneExists/DoesNotExist extensions, runComposeUiTest (expect) |
ui-test/src/commonMain/kotlin/androidx/compose/ui/test/v2/ComposeUiTest.kt |
v2 runComposeUiTest (StandardTestDispatcher default) |
ui-test-cm/ExperimentalTestApi.kt |
@ExperimentalTestApi, @InternalTestApi |
ui-test/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/IdlingResource.kt |
interface IdlingResource { val isIdleNow: Boolean; fun getDiagnosticMessageIfBusy(): String? = null } |
| File | Owns |
|---|---|
ui-test/src/androidMain/kotlin/androidx/compose/ui/test/MainTestClockImpl.android.kt |
Android clock impl |
ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeUiTest.android.kt |
runComposeUiTest, runAndroidComposeUiTest, runEmptyComposeUiTest, AndroidComposeUiTestEnvironment, sealed interface AndroidComposeUiTest<A> : ComposeUiTest (adds val activity: A?) |
ui-test/src/androidMain/kotlin/androidx/compose/ui/test/v2/ComposeUiTest.android.kt |
v2 actuals |
ui-test/src/androidMain/kotlin/androidx/compose/ui/test/ComposeIdlingResource.android.kt |
Aggregates recomposer + snapshot + frame-clock awaiters into one IdlingResource. Caps internal advance loop at 100 frames per call |
ui-test/src/androidMain/kotlin/androidx/compose/ui/test/IdlingStrategy.android.kt, EspressoLink.android.kt, RobolectricIdlingStrategy.android.kt |
Strategy split: Espresso for instrumentation, Robolectric for host. EspressoLink is the androidx.test.espresso.IdlingResource bridge |
ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidSynchronization.android.kt |
runOnUiThread posts a FutureTask via Instrumentation.runOnMainSync; if already on UI thread, runs in-place |
| File | Owns |
|---|---|
ui-test-junit4/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule.jvmAndAndroid.kt |
interface ComposeTestRule : TestRule, SemanticsNodeInteractionsProvider, interface ComposeContentTestRule : ComposeTestRule { fun setContent(...) }, expect createComposeRule |
ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt |
class AndroidComposeTestRule<R: TestRule, A: ComponentActivity>, actual createComposeRule(), createAndroidComposeRule<A>() (reified + non-reified), createEmptyComposeRule() |
ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/StateRestorationTester.android.kt |
class StateRestorationTester(rule: ComposeContentTestRule), setContent, emulateSavedInstanceStateRestore() |
ui-test-junit4/src/jvmAndAndroidMain/kotlin/androidx/compose/ui/test/junit4/v2/ComposeTestRule.jvmAndAndroid.kt |
v2 expect createComposeRule |
ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/v2/AndroidComposeTestRule.android.kt |
v2 createComposeRule / createAndroidComposeRule / createEmptyComposeRule (StandardTestDispatcher default) |
| File | Owns |
|---|---|
ui-test-manifest/src/main/AndroidManifest.xml |
The whole artifact: declares <activity android:theme="@android:style/Theme.Material.Light.NoActionBar" android:name="androidx.activity.ComponentActivity" android:exported="true" /> |
ui-test-manifest/build.gradle |
api("androidx.activity:activity:1.2.1"), lintPublish(:compose:ui:ui-test-manifest-lint) |
| File | Owns |
|---|---|
ui-test-manifest-lint/src/main/java/androidx/compose/ui/test/manifest/lint/GradleDebugConfigurationDetector.kt |
The TestManifestGradleConfiguration lint check: WARNING when androidx.compose.ui:ui-test-manifest is added on any config other than debugImplementation. Quick fix replaces it with debugImplementation |
| File | Owns |
|---|---|
ui-test-accessibility/src/androidMain/kotlin/androidx/compose/ui/test/accessibility/ComposeUiTestExt.android.kt |
@RequiresApi(34) @ExperimentalTestApi fun ComposeUiTest.enableAccessibilityChecks(validator), disableAccessibilityChecks |
ui-test-junit4-accessibility/src/androidMain/kotlin/androidx/compose/ui/test/junit4/accessibility/AndroidComposeTestRuleExt.android.kt |
Same on AndroidComposeTestRule<R, A> (NOT experimental — stable) |
| File | Why it's a canonical example |
|---|---|
compose/material3/material3/src/androidDeviceTest/.../SwitchTest.kt |
Class skeleton, hoisted state, position assertions |
compose/foundation/foundation/integration-tests/lazy-tests/.../LazyListTest.kt |
LazyList scrolling, scrollMainAxisBy, per-item tags |
compose/foundation/foundation/integration-tests/lazy-tests/.../LazyListItemPlacementAnimationTest.kt |
mainClock.autoAdvance = false + onAnimationFrame helper |
compose/animation/animation/src/androidDeviceTest/.../CrossfadeTest.kt |
advanceTimeBy + advanceTimeUntil, runOnUiThread { state = … } |
compose/material3/material3/src/androidDeviceTest/.../ButtonTest.kt |
getUnclippedBoundsInRoot math for padding |
compose/foundation/foundation/src/androidDeviceTest/.../DraggableTest.kt |
High-level swipe(start, end, durationMillis) |
compose/foundation/foundation/src/androidDeviceTest/.../ScrollableAreaTest.kt |
Low-level down(); moveBy() |
compose/foundation/foundation/src/androidDeviceTest/.../CombinedClickableTest.kt |
longClick, mouse hover via enter/exit |
compose/foundation/foundation/src/androidDeviceTest/.../TransformableTest.kt |
pinch(...) four-point gesture |
compose/foundation/foundation/src/androidDeviceTest/.../BasicTextFieldTest.kt |
performTextInput + assertTextEquals, performTextReplacement |
compose/foundation/foundation/src/androidDeviceTest/.../DefaultKeyboardActionsTest.kt |
performImeAction + IME-action focus chain |
compose/foundation/foundation/src/androidDeviceTest/.../TextFieldFocusCustomDialogTest.kt |
Compose ↔ Espresso interop: Espresso.onView(...).perform(ViewActions.click()) after rule.setContent |
compose/material3/material3/src/androidDeviceTest/.../SnackbarHostTest.kt |
rule.waitUntil { job.isCompleted } over a coroutine |
compose/material3/material3/src/androidDeviceTest/.../ExposedDropdownMenuTest.kt |
rule.waitUntil { matcher.matches(node) } |
compose/material3/material3/src/androidDeviceTest/.../ToggleButtonScreenshotTest.kt |
The ONE place Thread.sleep is correct: waiting on RenderThread for ripples |
compose/ui/ui-test/src/androidHostTest/.../RobolectricComposeTest.kt |
Host (Robolectric) entry-point, @Config(minSdk = RobolectricMinSdk = 23) |
commonTest
└── androidCommonTest (helpers shared by host + device)
├── androidDeviceTest (instrumentation APK on emulator/device)
└── androidHostTest (Robolectric on JVM)
androidDeviceTestrequires its ownAndroidManifest.xmlfor any non-ComponentActivitytest activity.androidHostTestusesorg.robolectric:robolectric+@Config(minSdk = …).enableRobolectric()is an internal androidx Gradle helper; consumers add Robolectric themselves.- Same
runComposeUiTest { setContent { … } }block runs unchanged in both — only the underlying Looper/Choreographer differs.
DefaultFrameDelay = 16_000_000Lns (16 ms) —TestMonotonicFrameClock.jvmAndAndroid.kt:33.MainTestClockandRecomposershare onekotlinx.coroutines.test.TestCoroutineScheduler.advanceTimeBy(ms)rounds up to nearest multiple of 16 ms unlessignoreFrameDuration = true.- Per-frame order inside the recomposer: animations awaiting
withFrameNanosresume before recomposition runs (MainTestClock.kt:49-60KDoc).
| Mode | What advances time? | When to use |
|---|---|---|
true (default) |
Framework auto-advances during waitForIdle/waitUntil (up to 100 frames per call inside ComposeIdlingResource.isIdleNow). |
Normal "drive UI to final state" tests. |
false |
Only explicit mainClock.advanceTimeBy* / advanceTimeUntil advance. waitForIdle does NOT advance. Pending recomposition / animations are NOT treated as non-idle. |
Animation tests, frame-by-frame snapshots, infinite animations (which would otherwise be cancelled by InfiniteAnimationPolicy). |
runOnUiThread { … }— posts to UI thread, blocks until done. Does NOT wait for idle.runOnIdle { … }=waitForIdle()thenrunOnUiThread. Default for state mutations.runWhenIdle { … }— same asrunOnIdlebut suppresses the implicitwaitForIdletriggered by node queries inside the block. Faster for assert-only blocks; MUST NOT mutate state inside.awaitAndRunWhenIdle { … }— suspending variant.
| API | Timeout source |
|---|---|
waitForIdle / awaitIdle |
Wall clock — Espresso IdlingPolicies.getMasterIdlingPolicy() (override with IdlingPolicies.setMasterPolicyTimeout) |
waitUntil(...) family |
Wall clock — per-call timeoutMillis = 1_000L default |
MainTestClock.advanceTimeUntil(...) |
Test clock — per-call timeoutMillis = 1_000 default |
runComposeUiTest(testTimeout = …) |
Wall clock — default 60.seconds. Throws AndroidComposeUiTestTimeoutException |
useUnmergedTreedefaults tofalseeverywhere.- Merged tree collapses descendants whose semantics roll up (e.g.
TextandIconinside aButtonshow as one node). - The flag is sticky —
onChild/onParent/filterpropagate it (SemanticsNodeInteraction.kt:42-49). - Error messages on count mismatch automatically run the same query against the unmerged tree and embed the unmerged matches as a hint to flip the flag.
- v1 (
androidx.compose.ui.test.junit4.createComposeRule,androidx.compose.ui.test.runComposeUiTest) —UnconfinedTestDispatcherfor composition. DeprecatedWARNING. Eager dispatch. - v2 (
androidx.compose.ui.test.junit4.v2.createComposeRule,androidx.compose.ui.test.v2.runComposeUiTest) —StandardTestDispatcherfor composition. Recommended. Matcheskotlinx.coroutines.test.runTestsemantics. Tests may need explicitmainClock.runCurrent()/advanceTimeBy(0)after migrating.
createComposeRule()requiresui-test-manifestasdebugImplementation. Without it:ActivityNotFoundExceptionforandroidx.activity.ComponentActivity. Lint flagTestManifestGradleConfigurationwarns when this dep is on the wrong config.- Custom Activity needs its own manifest entry in
src/androidTest/AndroidManifest.xml(orsrc/debug/AndroidManifest.xml). createEmptyComposeRule()returnsComposeTestRule, NOTComposeContentTestRule— nosetContentexposed; you must launch your own scenario.StateRestorationTester.emulateSavedInstanceStateRestore()does NOT exercise activity lifecycle — only restoresSaveableStateRegistry. Plainrememberstate is lost. Bundle size cap: 1 MB.- Cannot mix
runComposeUiTest { }andComposeTestRulein the same test — both manage independent test environments. runOnUiThreadfrom the UI thread does not deadlock (runs in-place viaisOnUiThread()check), butEspresso.runUntilIdlefrom the UI thread DOES throw with a clear message.Thread.sleepdesyncs fromMainTestClock— only legitimate use is waiting for the RenderThread (ripples, screenshot tests) where Compose has no idle observation.- Indefinite animations +
autoAdvance = true—InfiniteAnimationPolicy.onInfiniteOperationthrowsCancellationExceptionto prevent hangs. SetautoAdvance = falsefirst. - Accessibility checks require API 34 + are no-op on Robolectric — both
enableAccessibilityChecksextensions checkBuild.FINGERPRINT == "robolectric"and warn. performGestureis@Deprecated— alwaysperformTouchInput.
Five hot takes (see SPEC.md §5 for full text):
- Tag from production, find by tag from tests.
- Default to
useUnmergedTree = false. - Animation tests require
mainClock.autoAdvance = false. waitUntilis wall clock;advanceTimeUntilis test clock — do not mix.- Funnel state mutations through
runOnIdle/runOnUiThread.
Plus two recurring directives:
- Prefer the v2 entry points (
StandardTestDispatcherdefault). Thread.sleepis a smell except in screenshot/RenderThread waits.
Symptoms (write skills that match these):
- "test is flaky / sometimes passes" →
synchronizing-with-idle,testing-animations-deterministically - "animation never finishes / test times out" →
testing-animations-deterministically - "no node matched / multiple nodes matched" →
printing-the-semantics-tree,finding-nodes-by-tag-text-content - "ActivityNotFoundException" / "ComponentActivity not found" →
configuring-test-dependencies - "Cannot find test rule, createComposeRule unresolved" →
configuring-test-dependencies - "test passes locally but fails on CI" →
synchronizing-with-idle,setting-up-host-vs-device-tests - "InfiniteAnimationPolicy CancellationException" →
testing-animations-deterministically - "rememberSaveable state lost on rotation" →
testing-state-restoration - "Espresso onView and Compose at the same time" →
testing-with-espresso-interop - "Thread.sleep in test" →
synchronizing-with-idle - "merged tree vs unmerged tree" →
finding-nodes-by-tag-text-content - "performGesture deprecated" →
injecting-touch-gestures - "lint warning ui-test-manifest" →
configuring-test-dependencies - "Robolectric vs instrumentation" / "host test" →
setting-up-host-vs-device-tests - "accessibility checks throw on Robolectric" →
enabling-accessibility-checks
APIs (skill keyword sets):
createComposeRule,createAndroidComposeRule,createEmptyComposeRule,runComposeUiTest,runAndroidComposeUiTest,ComposeTestRule,ComposeUiTest,AndroidComposeTestRule,StateRestorationTesteronNodeWithTag,onNodeWithText,onNodeWithContentDescription,onAllNodes,onRoot,useUnmergedTreeSemanticsMatcher,hasText,hasClickAction,hasScrollToKeyAction,hasImeAction,isFocused,isEnabled,isOn,isPopup,isDialogassertExists,assertIsDisplayed,assertIsEnabled,assertIsOn,assertTextEquals,assertCountEquals,assertWidthIsEqualTo,getUnclippedBoundsInRootperformClick,performTouchInput,performMouseInput,performKeyInput,performMultiModalInput,performScrollToIndex,performScrollToKey,performScrollToNode,performTextInput,performTextReplacement,performImeActionMainTestClock,mainClock.autoAdvance,advanceTimeByFrame,advanceTimeBy,advanceTimeUntil,waitForIdle,waitUntil,waitUntilNodeCount,runOnIdle,runOnUiThread,runWhenIdleIdlingResource,IdlingResourceRegistry,ComposeIdlingResource,EspressoLink,IdlingPolicies.setMasterPolicyTimeoutenableAccessibilityChecks,AccessibilityValidator,tryPerformAccessibilityChecksprintToLog,printToString,fetchSemanticsNode,Modifier.testTag
Authoritative source: developer.android.com/training/testing/fundamentals + tasks/research/R8-android-fundamentals.md.
- https://developer.android.com/training/testing/fundamentals
- https://developer.android.com/training/testing/fundamentals/what-to-test
- https://developer.android.com/training/testing/fundamentals/test-doubles
- https://developer.android.com/training/testing/fundamentals/strategies
- https://developer.android.com/training/testing/local-tests
- https://developer.android.com/training/testing/instrumented-tests
- https://developer.android.com/training/testing/instrumented-tests/stability — determinism guidance (separate page)
Three sizes per Google: small (no Android stubs), medium (Android framework via Robolectric or in-process), big (instrumented). Plus an alternative 5-layer framing on /strategies: Unit / Component / Feature / Application / Release Candidate. Skill authors should pick ONE framing per skill and stick with it.
- Fake — a working implementation with shortcuts (e.g.
FakeUserRepositorywith in-memory map). Google explicitly prefers fakes over other doubles. - Mock — verifies interactions (calls, args, order).
- Stub — returns canned answers, no behavior.
- Spy — wraps a real object to record calls.
- Dummy — placeholder, never used.
- Shadow — Robolectric-specific; replaces an Android framework class with a JVM impl.
Authoritative quotes: "fakes ... are preferred", "Fakes are preferred over stubs for simplicity", "Fakes or mocks are therefore preferred over spies" — all from /test-doubles. Cite verbatim in skills.
Categories:
- State on screen — what's user-observable now.
- State held in memory —
ViewModelstate, in-memory caches. - Persisted state — DB, DataStore, SharedPreferences, files.
- Other state — system bars, system services, alarms.
- Errors and edge cases — empty lists, network failures, malformed input.
What NOT to test: implementation details, framework internals, third-party library guarantees.
src/test/— JVM unit tests.testImplementationdeps. Run with./gradlew test.src/androidTest/— instrumented tests.androidTestImplementationdeps. Run with./gradlew connectedAndroidTest.src/sharedTest/— community/Codelab convention (NOT Google-documented as such). Same source compiled into bothtestandandroidTest. Broken on AGP 7.2+ in many projects; Compose internally moved to KMPandroidHostTest/androidDeviceTest.
@HiltAndroidTest, HiltAndroidRule, @TestInstallIn, @UninstallModules, @BindValue, @CustomTestApplication. Rule ordering: HiltAndroidRule must be evaluated FIRST (@Rule(order = 0)).
- The "70/20/10 ratio" famous in older Google guidance is not on the current /fundamentals or /strategies pages. Cite Software Engineering at Google if needed; do NOT attribute to developer.android.com.
- "Hermetic" is not formalized on /strategies — only mentioned in passing.
- Given-When-Then / AAA are never NAMED on the pages but are visibly used in code samples. Cite the samples.
- Determinism / seed guidance lives at /instrumented-tests/stability, not /strategies.
- The FakeUserRepository sample on /test-doubles has a typo (
val const UserAliceis invalid Kotlin) — do not blindly reproduce.
testImplementation("junit:junit:4.13.2")
testImplementation("androidx.test:core:1.7.0")
testImplementation("androidx.test:runner:1.7.0") // primarily androidTestImpl, but referenced from JVM
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")
// Mockito
testImplementation("org.mockito:mockito-core:5.x") // 5+ uses inline by default
testImplementation("org.mockito.kotlin:mockito-kotlin:5.x") // Kotlin DSL
// MockK
testImplementation("io.mockk:mockk-jvm:1.14.x")
androidTestImplementation("io.mockk:mockk-android:1.14.x")
androidTestImplementation("io.mockk:mockk-agent:1.14.x")
// Coroutines test
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.x")
// Turbine
testImplementation("app.cash.turbine:turbine:1.x")
// Robolectric
testImplementation("org.robolectric:robolectric:4.x")
androidx.test.runner.AndroidJUnit4is@Deprecated— useandroidx.test.ext.junit.runners.AndroidJUnit4.androidx.test.InstrumentationRegistry(no.platform.app.) is@Deprecated— useandroidx.test.platform.app.InstrumentationRegistry.runBlockingTestis deprecated with error since coroutines 1.7+ — userunTest { }.ActivityTestRuleis deprecated — useActivityScenarioRule<A>.
Real usage signal from this checkout:
- Mockito-only — 400+ Kotlin files import
org.mockito, zero importio.mockk. Compose-foundation, compose-material, room3-runtime shipmockito-extensions/org.mockito.plugins.MockMakerfiles containingmock-maker-inline. - Canonical pattern:
mockito-kotlinDSL (mock<T>(),whenever,argumentCaptor<T>()withfirstValue/lastValue). - Coroutines test:
MainDispatcherRuleJUnit4 wrapper attestutils/testutils-ktx/src/jvmMain/kotlin/androidx/testutils/MainDispatcherRule.jvm.kt.
fun runTest(
context: CoroutineContext = EmptyCoroutineContext,
timeout: Duration = 60.seconds,
testBody: suspend TestScope.() -> Unit
): TestResultThe older dispatchTimeoutMs overload is deprecated with error. TestResult is Unit on JVM/Native, Promise<Unit> on JS — KMP tests must use fun foo() = runTest { … } single-expression form.
| Type | Behavior | When |
|---|---|---|
StandardTestDispatcher |
queues continuations; explicit advance | default; matches runTest semantics |
UnconfinedTestDispatcher |
eager dispatch | tests where you don't want to manage time |
AndroidJUnit4(fromandroidx.test.ext.junit.runners) is preferred — dispatches to Robolectric on JVM and to instrumentation on device. Enables sharedTest pattern.RobolectricTestRunneronly needed for Robolectric-specific subclasses (ParameterizedRobolectricTestRunner) or APIs that require it.@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])for single SDK; matrix@Config(sdk = [21, 28, 33])for multi-SDK.testOptions.unitTests.includeAndroidResources = trueis non-negotiable for any Robolectric test that touchesR.*.
androidTestImplementation("androidx.test:core:1.7.0")
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.fragment:fragment-testing:1.8.x")
debugImplementation("androidx.fragment:fragment-testing-manifest:1.8.x") // Android M+ manifest
// Espresso 3.7.0
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.7.0")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.7.0")
androidTestImplementation("androidx.test.espresso:espresso-web:3.7.0")
androidTestImplementation("androidx.test.espresso:espresso-accessibility:3.7.0")
implementation("androidx.test.espresso:espresso-idling-resource:3.7.0") // for production code
// UiAutomator
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
// Test Orchestrator (note: androidTestUtil, not androidTestImplementation)
androidTestUtil("androidx.test:orchestrator:1.6.1")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// Optional: pass args
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
testOptions {
execution = "ANDROIDX_TEST_ORCHESTRATOR" // for orchestrator
animationsDisabled = true // hermetic
}
}ActivityScenario.launch<MyActivity>(): ActivityScenario<MyActivity>
ActivityScenario.launch(intent: Intent): ActivityScenario<*>
ActivityScenario.launchActivityForResult<MyActivity>(): ActivityScenario<MyActivity>
scenario.moveToState(Lifecycle.State.STARTED) // CREATED, STARTED, RESUMED, DESTROYED
scenario.recreate()
scenario.onActivity { activity -> /* runs on UI thread */ }
scenario.state // current Lifecycle.State
scenario.result // Instrumentation.ActivityResult (only after finish)
scenario.close() // AutoCloseable; idempotent
// JUnit4 wrapper (the SINGLE canonical location):
@get:Rule val activityScenarioRule = ActivityScenarioRule(MyActivity::class.java)
// FQN: androidx.test.ext.junit.rules.ActivityScenarioRule
// NOT: androidx.test.rule.ActivityScenarioRule (does not exist)launchFragmentInContainer<MyFragment>(
fragmentArgs: Bundle? = null,
themeResId: Int = R.style.FragmentScenarioEmptyFragmentActivityTheme,
factory: FragmentFactory? = null
): FragmentScenario<MyFragment>
launchFragment<HeadlessFragment>(...) // no container; for headless
scenario.moveToState(Lifecycle.State.STARTED)
scenario.recreate()
scenario.onFragment { fragment -> ... }The default theme FragmentScenarioEmptyFragmentActivityTheme extends android:Theme.WithActionBar (NOT AppCompat). Override with themeResId for AppCompat fragments.
// Find
onView(withId(R.id.button))
onData(allOf(...)) // for AdapterView
// Match
onView(allOf(withText("Submit"), isEnabled()))
// Act
onView(...).perform(click())
onView(...).perform(typeText("hello"), closeSoftKeyboard())
// Assert
onView(...).check(matches(isDisplayed()))
onView(...).check(doesNotExist())
onView(...).check(matches(withText("Submit")))
// Idle
IdlingRegistry.getInstance().register(myIdlingResource)
IdlingPolicies.setMasterPolicyTimeout(60, TimeUnit.SECONDS)RootMatchers.DEFAULT excludes dialogs, popups, toasts. Always use .inRoot(isPlatformPopup()) for popup menus, .inRoot(isDialog()) for AlertDialog.
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.pressHome()
device.findObject(By.text("Submit")).click() // BySelector (modern)
device.findObject(By.res("com.example", "submit_btn")) // by package + res id
device.wait(Until.hasObject(By.text("Loaded")), 5000)
// Cross-app: launch Settings, etc.
device.executeShellCommand("am start -n com.android.settings/.Settings")StaleObjectException fires when a UiObject2 reference becomes invalid after recompose/relayout. Re-find with findObject(By...).
androidx.test.runner.AndroidJUnit4— deprecated; useext.junit.runners.AndroidJUnit4.ActivityTestRule— deprecated; useActivityScenarioRule.IntentsTestRule— deprecated; useIntentsRule(Espresso 3.5+) or callIntents.init() / .release()manually.UiSelector/UiObject— legacy; useBySelector/UiObject2.
Authoritative source: developer.android.com/tools/adb + developer.android.com/tools/logcat + tasks/research/A1, A2, A3.
- https://developer.android.com/tools/adb
- https://developer.android.com/tools/adb#wireless
- https://developer.android.com/tools/logcat
- https://developer.android.com/tools/releases/platform-tools
- https://developer.android.com/studio/test/command-line
- https://developer.android.com/studio/test/advanced-test-setup#use-gradle-managed-devices
- https://source.android.com/docs/setup/build/adb (AOSP)
- Client — the
adbCLI binary at$ANDROID_HOME/platform-tools/adb. - Server — background process on host, port 5037 by default.
- Daemon (
adbd) — runs on the device; spawned by the system or byiniton userdebug/eng builds.
adb start-server, adb kill-server, adb reconnect [device|offline]. Server logs at $TMPDIR/adb.$UID.log on macOS/Linux, %TEMP%\adb.log on Windows.
device (online), offline, unauthorized, recovery, sideload, bootloader, rescue, connecting, no permissions (Linux udev). The [-state] form requested in early plans does NOT exist; the canonical wait-for syntax is:
adb wait-for[-TRANSPORT]-<state>
TRANSPORT ∈ {usb, local, any}
state ∈ {device, recovery, rescue, sideload, bootloader, disconnect}
adb pair <host:port> # with pairing code from Settings → Developer options → Wireless debugging
adb connect <host:port> # once paired
adb disconnect [<host:port>]
Legacy path (pre-Android 11): adb tcpip <port> after USB, then adb connect. mDNS service types: _adb-tls-pairing._tcp, _adb-tls-connect._tcp. ADB v34+ default mDNS backend is Openscreen (not Bonjour) on Linux/Windows; doc lags.
adb shell am instrument -w -r \
-e class com.example.MyTest#myMethod \
-e package com.example \
-e size small \
-e numShards 4 -e shardIndex 0 \
-e annotation com.example.SmokeTest \
-e clearPackageData true \
com.example.test/androidx.test.runner.AndroidJUnitRunner-w(wait) is required for exit codes to be meaningful.-rraw output.- Output framing:
INSTRUMENTATION_STATUS(per-test),INSTRUMENTATION_STATUS_CODE(1 start, 0 ok, -1 error, -2 failure, -3 ignored, -4 assumption-failure),INSTRUMENTATION_RESULT(final),INSTRUMENTATION_CODE(final). - AndroidJUnitRunner adds
-3IGNORED and-4ASSUMPTION_FAILURE on top of the framework's 4. - For Test Orchestrator: target =
androidx.test.orchestrator/androidx.test.orchestrator.AndroidTestOrchestrator, with-e targetInstrumentation com.example.test/androidx.test.runner.AndroidJUnitRunner.
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
adb shell pm clear <pkg> # clean app state before each runOr via Gradle: testOptions.animationsDisabled = true.
adb logcat -d # dump and exit
adb logcat -c # clear
adb logcat MyTag:D *:S # only MyTag at D+, silence others
adb logcat *:E # errors and above
adb logcat -b crash # the crash buffer
adb logcat --pid $(adb shell pidof <pkg>) # PID filter
adb logcat -v threadtime # default; -v json on Android 11+
adb logcat -t 100 # last 100 lines then exitBuffers: main (default), system, crash, events (binary), radio, kernel, all.
| Path | Writable by adb shell? | Pull works? |
|---|---|---|
/data/local/tmp/ |
✓ | ✓ |
/sdcard/ (/storage/emulated/0/) |
✓ (within scoped storage rules on API 30+) | ✓ |
/data/data/<pkg>/ |
only via run-as <pkg> (debuggable builds) |
only via run-as |
/sdcard/Android/data/<pkg>/files/ |
package-owned, scoped | ✓ for the owning package |
Binary-clean grab on debuggable build: adb exec-out run-as <pkg> cat <path> > local-file.
adb forward tcp:<host_port> tcp:<device_port> # host → device. LOCAL REMOTE
adb reverse tcp:<device_port> tcp:<host_port> # device → host. REMOTE LOCAL
The two have opposite argument order. Most common scripting bug.
am instrumentexit codes only valid with-w. Without it,$?is meaningless.force-stopdoes NOT clear data — pair withpm clearfor hermetic reset.svc data,svc wifi,svc bluetoothare no-ops on API 30+. Usecmd wifi set-wifi-enabled,cmd connectivity airplane-modeinstead.adb shellexit-code propagation reliable only since API 24 / platform-tools 24.adb -t <transport-id>is transport ID, NOT a timeout flag. Wrap with shelltimeout(orgtimeouton macOS) for timeouts.adb logcat -f <file>writes host-side viaadb logcat, but device-side viaadb shell logcat -f. Subtle.- Test Orchestrator wires up via
androidTestUtil("androidx.test:orchestrator:..."), NOTandroidTestImplementation. - Quoting through
adb shellis double-shell (ssh-style) since Platform Tools 23 — load-bearing forinput textand any string with spaces.