Power Your Software Testing with AI Agents and Cloud
The Native AI-Agentic Cloud Platform to Supercharge Quality Engineering. Test Intelligently and Ship Faster.
- TestMu AI (Formerly LambdaTest)
- /
- Learning Hub
- /
- Android App Automation: Frameworks, Setup and CI
Android App Automation: How to Automate Android App Tests
Android app automation is the use of test frameworks such as Espresso, UI Automator, Appium and Maestro to run scripted checks of an Android app's screens and behavior on emulators or real devices, without manual tapping. This guide covers automating app testing, not automating tasks on your own phone.
Published on:
A login screen that worked on Monday can break on Wednesday because a layout change moved a button, a dependency update changed a dialog, or a new Android version tightened a permission. Checking every core flow by hand on every build stops being practical after a few releases, and the bugs that slip through tend to sit in the flows nobody had time to tap through.
Android app automation replaces those taps with code. Espresso, Google's native framework, runs inside the app process and synchronizes with the UI thread. UI Automator reaches outside the app to system dialogs, notifications and other apps. Appium drives Android through its UiAutomator2 driver over the W3C WebDriver protocol, so one suite can also cover iOS and can test a release build without source access. Maestro runs short YAML flows with waiting built in. Automation is one part of Android testing, which also covers unit, manual and performance work.
How Does Android App Automation Work?
An automated Android UI test either ships in a test APK that runs alongside your app on the device, or drives the installed app from outside the way a user would. The first style is called instrumented testing. The second is black-box testing, because the test sees only what is on the screen and never the app's code.
Neither style replaces fast local tests. Logic that does not touch the Android runtime, such as view models and validation rules, belongs in local unit tests that run on your machine's JVM in milliseconds. UI automation earns its cost on full screens and user journeys, where only a running app shows whether the pieces fit.
Instrumented Tests: Inside the App's Build
The Android testing fundamentals guide defines instrumented tests as tests that run on an Android device, either physical or emulated, where the app is built and installed alongside a test app that injects commands and reads the state. They live under src/androidTest, run through AndroidJUnitRunner, and can reference your app's resources directly, so a test finds the email field as R.id.email instead of guessing at screen text.
Espresso and the Jetpack Compose test APIs work this way. Because the test shares a process with the app, the framework can see when the main thread is busy and wait for it, which removes most of the timing guesswork from a UI test. UI Automator also ships in the test APK, but it acts on the device from outside the app's process, which lets it test release builds with minification applied and reach screens your app does not own.
Black-Box Tests: Driving the Installed App
Appium is an HTTP server written in Node.js. Your test, written with an Appium client library, sends W3C WebDriver commands to the server, and the server hands them to a platform driver. On Android that is the UiAutomator2 driver, which, according to the UiAutomator2 driver documentation, proxies most commands to a UiAutomator2 server on the device that uses Google's UiAutomator framework under the hood. The test needs only the APK, so QA engineers can automate a release build without access to the source tree.
Maestro takes the same outside-in approach with less setup. Its maintainers describe it as driving the app through the platform's accessibility layer and installing as a single binary with no drivers or SDK. Tests are flat YAML files that Maestro interprets at run time, so there is no compile step.
The trade-off is synchronization. An instrumented test knows when the app is idle. A black-box test has to infer it from the screen, and Android's guide to test stability calls these synchronization issues a primary source of flakiness, because the framework must deduce whether an activity is done loading or should wait longer. That is why explicit waits matter more in Appium and why Maestro builds waiting into every command.
Which Android App Automation Framework Should You Use?
Pick Espresso when developers test their own app, UI Automator for system UI and cross-app steps, Appium when one suite must cover Android and iOS or test a release build without source, and Maestro for readable YAML flows with built-in waiting. Many teams combine two: Espresso in the app repository and Appium or Maestro for end-to-end journeys owned by QA.
| Framework | How It Runs | Test Language | Needs App Source | Best For |
|---|---|---|---|---|
| Espresso | Instrumented, inside the app process | Kotlin, Java | Yes | Developers testing their own app's screens on every commit |
| UI Automator | Instrumented, acting from outside the app process | Kotlin, Java | No for the app under test; tests live in an Android project | Permission dialogs, notifications, settings and cross-app steps |
| Appium (UiAutomator2 driver) | Black-box, through a WebDriver server | Java, Python, Ruby, C#, JavaScript via WebdriverIO | No, an APK is enough | QA teams running one suite on Android and iOS, and release-build checks |
| Maestro | Black-box, through the accessibility layer | YAML flows | No | Readable end-to-end flows, including React Native and Flutter apps |
| Our platform: KaneAI by TestMu AI | Authors tests from plain-English steps and runs them on real devices in the cloud | Natural language, with Appium code export | No | Teams that want mobile tests written in plain English instead of framework code |
Disclosure: TestMu AI publishes this guide. KaneAI is our product, so it sits in its own labelled row and is not ranked against the open-source frameworks.
Espresso - The Espresso documentation explains that each onView() call waits until the message queue has nothing Espresso must process immediately, no AsyncTask is running, and every developer-defined idling resource is idle. That check is what lets Espresso tests drop sleeps and polls. Apps built with Jetpack Compose use the Compose test APIs, which follow the same in-process model. The Espresso tutorial series covers matchers, intents and idling resources in depth.
UI Automator - Google's UI Automator guide describes APIs for UI tests that interact with user apps and system apps, and notes that it can test release versions with minification applied. The stability guide adds a caution: UI Automator tests can need more manual synchronization, so they tend to be less reliable than Espresso or Compose tests. Use it for the steps Espresso cannot reach, such as granting a runtime permission or opening a notification.
Appium - The current Appium server needs Node.js in the range ^20.19.0, ^22.12.0 or 24.0.0 and later, per the Appium requirements page. Since UiAutomator2 driver version 6.0.0, the minimum supported Android version is 8 (API level 26), so very old devices need an older driver. The Appium team maintains official clients for Java, Python, Ruby and C#.
Maestro - The Maestro project README lists built-in flakiness tolerance and automatic waiting, a Java 17 or higher requirement, and support for emulators, simulators, browsers and physical Android devices, with physical iOS devices not yet supported. The guide to Maestro testing walks through flows, selectors and Maestro Studio.
Your First Automated Android Test: Espresso and Appium Side by Side
Both tests below check the same behavior: a user enters valid credentials, taps the login button, and lands on the home screen. The example app has the application ID com.example.app and views with the IDs email, password, login_button and home_title. Swap in your own IDs and class names.
Espresso setup - Add the AndroidX Test dependencies and the instrumentation runner to the app module, as the Espresso setup guide describes. The androidx.test.ext:junit artifact supplies ActivityScenarioRule and the AndroidJUnit4 runner class.
// app/build.gradle.kts
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
}Appium setup - Install the server and the UiAutomator2 driver, check the machine with the driver's doctor command, and install the Python client. The UiAutomator2 quickstart also asks for an Android SDK with ANDROID_HOME set and a JDK with JAVA_HOME set.
npm install -g appium
appium driver install uiautomator2
appium driver doctor uiautomator2
pip install Appium-Python-Client
# start the server (listens on http://127.0.0.1:4723)
appiumEspresso (Kotlin, instrumented)
// app/src/androidTest/java/com/example/app/LoginTest.kt
package com.example.app
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Test
fun validLogin_showsHomeScreen() {
onView(withId(R.id.email))
.perform(typeText("qa@example.com"), closeSoftKeyboard())
onView(withId(R.id.password))
.perform(typeText("secret"), closeSoftKeyboard())
onView(withId(R.id.login_button)).perform(click())
onView(withId(R.id.home_title))
.check(matches(isDisplayed()))
}
}Appium (Python, black-box)
# test_login.py
import unittest
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
APPIUM_SERVER = "http://127.0.0.1:4723"
APP_ID = "com.example.app"
class LoginTest(unittest.TestCase):
def setUp(self):
options = UiAutomator2Options()
options.device_name = "Android Emulator"
options.app = "/path/to/app-release.apk"
self.driver = webdriver.Remote(APPIUM_SERVER, options=options)
def tearDown(self):
self.driver.quit()
def test_valid_login_shows_home_screen(self):
d = self.driver
d.find_element(AppiumBy.ID, f"{APP_ID}:id/email").send_keys("qa@example.com")
d.find_element(AppiumBy.ID, f"{APP_ID}:id/password").send_keys("secret")
d.find_element(AppiumBy.ID, f"{APP_ID}:id/login_button").click()
home = WebDriverWait(d, 10).until(
EC.visibility_of_element_located((AppiumBy.ID, f"{APP_ID}:id/home_title"))
)
self.assertTrue(home.is_displayed())
if __name__ == "__main__":
unittest.main()Run the Espresso test with an emulator or device connected by calling ./gradlew connectedAndroidTest. Gradle builds both APKs, installs them, runs the suite, and writes an HTML report to build/reports/androidTests/connected/ in the module. Run the Appium test by starting the server with appium in one terminal and python test_login.py in another.
The two files show the practical difference between the styles:
- Locators - Espresso uses the compiled resource reference R.id.email, so a renamed ID fails at build time. Appium uses the full resource-id string com.example.app:id/email, which only fails when the test runs.
- Waiting - The Espresso test has no explicit wait, because onView() waits for the main thread and any registered idling resources. If your login call runs on a background thread Espresso does not track, register an idling resource for it. The Appium test adds an explicit WebDriverWait for the home title, since the server cannot know when the login request has finished.
- What gets tested - The Espresso test launches LoginActivity directly from the test APK. The Appium test installs and starts the APK you point it at, which can be the release build your users receive.
- Reuse across platforms - The Appium test structure carries over to iOS with the XCUITest driver and iOS locators. The Espresso test is Android only.
The same check in Maestro is a short YAML flow. It assumes the fields show the hint text "Email" and "Password", the button reads "Log in", and the home screen shows "Welcome".
# login.yaml (run with: maestro test login.yaml)
appId: com.example.app
---
- launchApp
- tapOn: "Email"
- inputText: "qa@example.com"
- tapOn: "Password"
- inputText: "secret"
- tapOn: "Log in"
- assertVisible: "Welcome"For Appium Inspector, locator strategies and running the same test on a cloud grid, follow the Appium tutorial for Android apps, which builds a Java suite step by step.
Should You Automate on Emulators or Real Devices?
Use emulators for fast feedback on every commit and real devices before release. Both run the same Espresso, UI Automator, Appium and Maestro tests, so the choice is about where each run gives the most useful signal, not about rewriting tests.
Emulators are cheap to scale and easy to reset. Gradle can create, boot and tear them down for you, and the Gradle managed devices guide says it uses emulator snapshots to speed up startup and to restore devices to a clean state between tests. The Automated Test Device (ATD) images go further: they remove preinstalled apps that are not useful for testing, disable some background services, and turn off hardware rendering. The same guide warns that screenshot tests depending on hardware rendering are not supported on ATDs, so keep visual checks on a standard image.
Real devices show what an emulator cannot. Manufacturer skins change system dialogs, keyboards and background limits. Cameras, sensors, biometric hardware and thermal throttling exist only on physical phones, and a low-memory handset on a weak mobile network exposes slow screens and dropped requests that a desktop-hosted emulator hides. Choose which phones to buy or rent from your analytics, following the method in the guide to building an Android device matrix, rather than from a list of popular models.
Owning that matrix is the expensive part. TestMu AI App Automation runs Appium and Espresso suites on 10,000+ real devices plus cloud emulators, in parallel, and records video, device logs and network logs for every session, so a failure on one vendor's phone arrives with the evidence needed to reproduce it.
How to Run Android App Automation in CI
A UI suite that only runs on a developer's laptop stops catching regressions within a few weeks. The goal in CI is a clean, repeatable emulator on every pull request, a report you can open when a test fails, and a runtime short enough that nobody is tempted to skip it.
Gradle Managed Devices
Gradle managed devices, which Android's documentation now also calls build-managed devices, let you declare a virtual device in the build file. The Android Gradle plugin creates, deploys and tears down that device when tests run. The feature supports API levels 27 and higher, handles device-related issues so tests still execute, and caches results so it reruns only tests likely to produce a different result.
// app/build.gradle.kts
android {
testOptions {
managedDevices {
localDevices {
create("pixel2api30") {
device = "Pixel 2"
apiLevel = 30
systemImageSource = "aosp-atd"
}
}
}
}
}
// run it: ./gradlew pixel2api30DebugAndroidTest
// shard it (gradle.properties):
// android.experimental.androidTest.numManagedDeviceShards=2The task name combines the device name, the build variant and AndroidTest, so the device above runs with ./gradlew pixel2api30DebugAndroidTest. Define several devices and add them to a named group when you need more than one API level or form factor, and the plugin runs the group's devices in parallel. The ATD image in this example is the API 30 image the Android documentation uses for ATDs.
Sharding Long Suites
Sharding splits one suite across several identical devices that run at the same time. With managed devices, set android.experimental.androidTest.numManagedDeviceShards in gradle.properties. The Android docs warn that this can be resource intensive, and a run fails with a timeout if Gradle cannot provision the requested devices, so start with two shards on a standard runner.
To spread a suite across separate machines or physical devices, use the runner's own sharding. The AndroidJUnitRunner documentation describes the numShards and shardIndex arguments, and each CI job runs a different index.
# split the suite into 4 shards and run the first one on this device
adb shell am instrument -w -e numShards 4 -e shardIndex 0 \
com.example.app.test/androidx.test.runner.AndroidJUnitRunnerGitHub Actions
GitHub announced in its April 2024 changelog that its 2-vCPU hosted Linux runners can use hardware acceleration for the Android emulator, once a workflow step gives the runner user access to KVM. The Gradle managed devices guide adds that servers without hardware rendering, such as GitHub Actions, need the swiftshader_indirect GPU flag. The workflow below combines both and uploads the reports even when tests fail.
# .github/workflows/android-ui-tests.yml
name: android-ui-tests
on: [pull_request]
jobs:
instrumented-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Enable KVM for the emulator
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Run tests on a Gradle managed device
run: ./gradlew pixel2api30DebugAndroidTest -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect
- uses: actions/upload-artifact@v4
if: always()
with:
name: android-test-reports
path: app/build/reports/androidTests/Appium suites follow the same pattern with two extra steps: start the Appium server in the background and wait until it answers before the tests begin. Alternatively, point the client's server URL at a cloud device grid and skip the local emulator entirely, which also removes the KVM step.
How Do You Fix Flaky Android Tests?
Most flaky Android UI tests come from timing guesses, animations and state that leaks between tests, and each has a specific fix. Android's stability guide frames the rule plainly: add retries to big tests, but always fix flaky tests. A broader treatment of root causes and detection is in the guide to flaky tests.
- Delete fixed sleeps - The stability guide says arbitrary sleeps slow tests down without removing flakiness, because the same test needs more or less time in different environments. Replace each one with a wait on a condition.
- Teach the framework about background work - Register Espresso idling resources for network calls and databases in small UI tests, and use wait-until APIs such as the Compose waitUntil family in bigger tests, as Android recommends. In Appium, wait for a visible element, not for a number of seconds.
- Turn off animations - The Espresso setup guide recommends disabling the window animation scale, transition animation scale and animator duration scale in Developer options. For command-line and CI runs, animationsDisabled = true in testOptions does the same.
- Isolate every test - Android Test Orchestrator runs each test in its own Instrumentation instance, so a crash takes down only that test, and the clearPackageData argument wipes app state after each one. The documentation notes the cost: runs take longer because the app restarts after each test.
- Keep the device quiet - System update dialogs and low disk space interrupt tests. Managed devices and clean snapshots avoid both, and device clouds usually configure their phones for testing.
- Use stable locators - Resource IDs and accessibility IDs survive copy changes and translations. XPath expressions built on screen text break whenever a label changes.
Orchestrator, state clearing and disabled animations fit in a few lines of the app module's build file:
// app/build.gradle.kts
android {
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// runs "pm clear" after each test so no state leaks between tests
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
testOptions {
execution = "ANDROIDX_TEST_ORCHESTRATOR"
animationsDisabled = true
}
}
dependencies {
androidTestUtil("androidx.test:orchestrator:1.6.1")
}What Should You Automate and What Should Stay Manual?
Automate the stable flows that must work on every build, and keep judgment calls and fast-changing features manual. Android's testing fundamentals note that bigger tests are slower, so testing every possible state through the UI is unrealistic. Push logic into local unit tests and save UI automation for journeys where a broken release would cost real users.
Good candidates for automation:
- Core journeys - Sign-up, login, search, checkout and any flow that blocks revenue or access when it breaks.
- Fixed bugs - A regression test for every UI bug you fix, so the same defect cannot ship twice.
- Version and screen coverage - The same flow on your oldest supported API level, the newest one, and a tablet layout.
- System interactions - Runtime permission prompts, notifications and deep links, using UI Automator or Appium.
- Data-heavy forms - Input validation across many values, driven from a data table instead of repeated taps.
Keep these manual, at least for now:
- Exploratory sessions - A person trying odd inputs and paths finds the bugs nobody wrote a test for.
- Usability and visual judgment - A script can confirm a button exists, not whether the screen is confusing or the layout looks broken on a small phone.
- Features still in flux - Automating a screen that changes every sprint creates maintenance work faster than it finds bugs. Automate once the design settles.
- Hardware-dependent quality - Camera output, audio quality and how the app feels under heat or a failing network still need a person holding a real phone.
The broader guide to mobile app testing covers how manual, automated and device testing fit into one release process for Android and iOS.
Next Steps for Your First Android Suite
Start with one test for your most important journey, usually login. Write it in Espresso if the developers own testing, or in Appium or Maestro if QA does, and make it pass locally on an emulator. Add a Gradle managed device and the GitHub Actions workflow so it runs on every pull request, then grow the suite one regression at a time, fixing each flaky test as soon as it appears.
When the emulator runs are green and stable, add a real-device pass on release candidates across the phones in your device matrix. TestMu AI runs the same Appium and Espresso suites on real Android devices in the cloud: Appium tests need a new server URL and capabilities, and Espresso tests run from the app and test APKs you already build.
Author
Navin Chandra is a Member of Technical Staff at TestMu AI, building the open-source automation that powers its Selenium and Appium cloud grid. A committer to both Selenium and Appium, he implemented WebDriver BiDi support in Selenium for real-time browser events and bidirectional control and is developing Apple's iOS RemoteXPC protocol in Appium to enable low-level wireless communication with iOS system services. He contributes to Selenium across multiple language bindings as a member of the Selenium GitHub organization. He has served as a Google Summer of Code mentee and mentor at openSUSE and an LFX mentee at CNCF's KubeArmor, and is a SUSE Certified Deployment Specialist. Navin holds a B.Tech in Computer Science.
Android App Automation FAQs
Did you find this page helpful?
More Related Learning Hubs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance
- Advanced access controls
- Advanced data retention rules
- Advanced Local Testing
- Premium Support options
- Early access to beta features
- Private Slack Channel
- Unlimited Manual Accessibility DevTools Tests





