Android UI Automator: How to Automate UI Tests
Set up Android UI Automator 2.4 in Kotlin, automate cross-app and system UI flows, and choose the right selectors, waits, and test runner.
Android UI Automator runs instrumented tests on an emulator or Android device and interacts with visible UI across app boundaries. It is a good fit for flows involving permission dialogs, Settings, the launcher, notifications, or another installed app. For new tests, use the modern Kotlin DSL with UI Automator 2.4.0, select elements by stable properties, and assert a visible outcome.
1. Decide whether UI Automator fits the test
Choose the test framework based on what the scenario crosses and how the app is built.
| Framework | Use it when |
|---|---|
| UI Automator | The user journey crosses into system UI or another app, or needs opaque-box interaction with visible UI. |
| Espresso | The test is primarily View-based interaction inside one app and benefits from synchronization with the app’s main thread. |
| Compose testing APIs | The test targets Compose screens or components and needs Compose-specific control over time, animations, or recomposition. |
| Robolectric | The test should run as a local JVM test on a workstation or CI host rather than on a device or emulator. |
These tools are not interchangeable. Prefer the framework designed for the UI toolkit and execution boundary in the scenario. Android recommends Espresso for common View tests within one app and dedicated Compose APIs for Compose interfaces. Android’s UI behavior testing guidance explains the distinctions.
2. Set up an instrumented Kotlin test
Use the stable AndroidX UI Automator 2.4.0 dependency. The modern guide’s sample has shown an alpha coordinate, so use the release notes for the current stable version. Put the test under src/androidTest/java (or the matching Kotlin source directory), and configure AndroidJUnitRunner.
// app/build.gradle.kts
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0")
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
}
The UI Automator dependency is the essential addition. The runner and AndroidX JUnit dependencies are shown explicitly for a typical instrumented-test setup; if the project already manages compatible test dependencies through a version catalog or convention plugin, follow that setup. Android’s UI Automator release notes list 2.4.0 as stable, dated July 1, 2026. See also Build instrumented tests.
Run the test on a booted emulator or connected device from Android Studio, or use Gradle:
./gradlew connectedDebugAndroidTest
Instrumented tests execute on the target Android environment. Ensure the test APK and app APK can be installed and that the selected device is online and unlocked as your test setup expects.
3. Write a Kotlin test with the modern DSL
This example starts the app, finds a visible button by its text, taps it, and checks for the expected result. Replace the package name and visible text with values from your app. Keep UI Automator’s DSL calls inside the uiAutomator scope.
package com.example.app
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.uiAutomator
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.assertNotNull
@RunWith(AndroidJUnit4::class)
class CheckoutUiTest {
@Test
fun checkoutButtonOpensConfirmation() = uiAutomator {
startApp("com.example.app")
val checkout = onElement {
textAsString() == "Checkout"
}
checkout.click()
val confirmation = onElementOrNull {
textAsString() == "Order confirmed"
}
assertNotNull("Confirmation should be visible after checkout", confirmation)
}
}
The 2.4 DSL centers on uiAutomator {}, predicate-based lookup through onElement, onElements, and onElementOrNull, and app-state operations such as startApp, startActivity, and clearAppData. Verify exact imports and available predicates against the API reference for the version in your project: Write automated tests with UI Automator.
Use the lookup form that matches your assertion. onElement is for an element expected to exist and uses conditional waiting; onElementOrNull is useful when absence is a valid result to check. Use onElements when the scenario intentionally evaluates multiple matches. Avoid using a nullable lookup and then asserting without a useful failure message.
4. Choose robust selectors and actions
Prefer selectors that describe what the user sees or a stable app resource:
- Resource ID: usually the most stable choice when the app exposes a meaningful ID.
- Text: useful for user-visible labels, but can change with copy edits or locale.
- Content description: useful for accessible controls and icon buttons; make descriptions meaningful and localized.
- Coordinates: use only when semantic properties are unavailable or the target is inherently spatial. Coordinates are sensitive to layout, density, orientation, and system insets.
Make important controls discoverable by providing visible text labels or appropriate android:contentDescription values. A selector that matches several elements can tap the wrong one or fail ambiguously; narrow it with a parent, ID, or other stable property. For localized tests, use the expected locale’s strings rather than assuming English text.
Use the modern API’s element operations for interaction and verification. Start the app or activity explicitly so the test begins from a known state. Clear app data only when the scenario requires first-run state; clearing data can remove useful setup and makes tests slower.
5. Handle system dialogs and cross-app flows
UI Automator is especially useful when a test crosses a boundary the app’s own in-process UI test cannot own, such as a runtime permission surface or Settings. Establish the expected app state, perform the action that triggers the system UI, interact with the visible dialog, and then assert the state in the app.
For an interruption that is genuinely expected, register a watcher with watchFor and make its action specific. Do not create a broad watcher that automatically dismisses any dialog: it can hide a real failure or change the meaning of the test. Use the modern API’s app lifecycle operations to launch the target app or activity and to return to a known state as needed. The official guide also documents waiting for app visibility and window-node approaches for multi-window scenarios.
System dialog text and behavior can vary by Android version, locale, app permissions already granted, and device configuration. Arrange the preconditions deliberately. A permission test should cover the state it intends to test (first request, granted, or denied) instead of relying on whatever state a reused emulator happens to have.
6. Wait for conditions, not arbitrary time
Modern UI Automator lookups include conditional waiting. Use that behavior for elements that should appear after a user action. Where the scenario needs it, the DSL also offers tools such as waitForAppToBeVisible and waitForStable.
Stability has a limit: a stable accessibility tree does not prove that background work, network requests, animations, or rendering have all finished. Follow a stability wait with the actual condition the user cares about, such as a result label appearing or a loading indicator disappearing. Avoid fixed sleeps as the primary synchronization method; they waste time on fast runs and may still be too short on slow ones.
7. Run against a useful compatibility matrix
UI behavior can vary with platform and configuration. Select coverage based on the app’s users and supported behavior:
- Include relevant Android API levels, especially where system UI or permission behavior differs.
- Exercise portrait and landscape if the app supports both.
- Run important localized flows in the locales the app supports.
- Include relevant form factors, such as tablets, when layouts or interactions differ.
- Use emulators for repeatable broad coverage and physical devices when real-device behavior matters.
No special phone model is required by UI Automator. Android supports running the instrumented workflow on emulators as well as devices. For broader planning, see Automate UI tests.
8. Legacy API and migration notes
Older UI Automator examples use the 2.3-era APIs such as UiDevice, UiObject2, and BySelector. The modern 2.4 DSL has a different shape; do not combine snippets from the two styles without checking their imports and dependency versions. New tests should follow the current modern guide. Existing suites can be migrated incrementally, keeping each test on a consistent API style.
The legacy guide describes API 18 or higher for its documented framework guidance. That is not a compatibility guarantee for every current library version or project configuration. Confirm minimums against the exact AndroidX version and the devices your project supports. See Use the UI Automator legacy API.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Test class or DSL symbols do not resolve | Dependency missing, wrong source set, or code copied from a different API version. | Confirm the 2.4.0 AndroidX dependency, place code in the instrumented test source set, sync Gradle, and align imports with the modern guide. |
| Test runs as a local unit test or cannot connect to a device | Wrong task/runner or no usable emulator/device. | Configure AndroidJUnitRunner and run the connected instrumented test task with a booted, authorized target. |
| Element lookup times out | Wrong text or ID, app not in expected state, element not accessible, or UI still changing. | Inspect the actual screen and accessibility properties, establish app state explicitly, use a stable selector, and wait for the expected condition. |
| Selector finds the wrong control | Duplicate text or an overly broad predicate. | Constrain by resource ID, parent, content description, or another property; assert uniqueness where appropriate. |
| System dialog is missing | Permission already granted/denied, dialog not triggered, or OS version/locale differs. | Reset only the state required by the scenario, verify the action triggers the dialog, and account for device and locale variations. |
| Test passes locally but fails on another configuration | Orientation, API level, form factor, locale, density, or timing changes the visible UI. | Use semantic selectors and explicit preconditions; add the relevant configurations to the matrix. |
| Test continues before content is ready | Accessibility-tree stability was treated as complete application idleness. | Wait for and assert the specific result or loading-state change; do not infer completion from stability alone. |
| Coordinate tap misses | Layout or system insets differ. | Use a semantic selector if possible. If coordinates are necessary, derive them from the current display/layout and cover relevant orientations and densities. |
10. Performance, reliability, and cost
Instrumented UI tests incur device or emulator startup, app installation, and UI interaction time, so reserve them for behavior that needs the real Android UI environment. Keep tests focused on meaningful user journeys, avoid unnecessary data clearing and fixed delays, and use stable selectors. Run a smaller relevant set during fast development loops and broader API/configuration coverage in scheduled or CI runs according to the team’s needs.
Reliability depends on deterministic preconditions and observable assertions. Control app state, permissions, locale, and orientation for the scenario; avoid selectors tied to incidental layout; and diagnose failures from the actual displayed/accessibility state. No success-rate or runtime benchmark is implied here. Emulator execution avoids device procurement for basic automation; physical hardware is an optional addition when compatibility behavior warrants it.
UI Automator is an AndroidX library and the cited Android documentation does not establish a separate usage fee. The practical costs are the compute and maintenance required to run and maintain the test environment; cloud device-provider pricing, if used, depends on that provider and is outside this guide’s evidence.
11. Capture a website screenshot from a test workflow
Android UI Automator tests Android interfaces. If the test workflow also needs a rendered website screenshot, use a browser capture service for that part instead of treating UI Automator as a website screenshot API. ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request can return a PNG, JPEG, WebP, or PDF; see the ScreenshotNeo site and its API documentation.
Or skip the browser setup
Call ScreenshotNeo with the target URL and access key:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with page verdict and billing information in response headers. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. Read the ScreenshotNeo docs and sign up for 1,000 free screenshots a month with no card.
FAQ
Does UI Automator require a physical Android phone?
No. Use an emulator or Android device. Physical hardware is useful when the behavior under test depends on real-device characteristics.
Can UI Automator test another app?
Yes. It is designed for visible UI interactions across app boundaries, including system UI and installed apps, subject to device state and permissions.
Should a Compose app use UI Automator for every screen test?
No. Use Compose testing APIs for Compose-focused component and screen tests; keep UI Automator for journeys that need device-level or cross-app interaction.
Is the 2.4.0 dependency stable?
The AndroidX release notes identify 2.4.0 as stable, dated July 1, 2026. Check the release page when upgrading for later releases.


