This page is for testers and SDETs facing a mobile automation round, from a first Appium job to a senior role owning the whole device lab. Most Appium interviews start with how the server, drivers and devices fit together, then move to capabilities, locators on Android and iOS, gestures and hybrid webviews. Later rounds test real devices versus emulators, parallel runs, flaky tests and framework design, and end with a story from your own work. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Client: the language library turns the call into a W3C WebDriver HTTP request.
Server: the Appium server finds the session and hands the command to that session's driver.
Device side: the driver forwards it to a helper on the device that uses the platform's own automation framework.
"My test uses a client library, say the Java client. When I call click, the client turns it into a W3C WebDriver HTTP request and sends it to the Appium server, which is a Node.js process listening on port 4723 by default. The server looks up my session and passes the command to the driver the session was created with, for example UiAutomator2 on Android. That driver has already installed a small helper server on the device, and it forwards the command there. On Android the helper uses Google's UiAutomator framework to find the element and tap it. On iOS the XCUITest driver does the same through WebDriverAgent, which is built on Apple's XCTest. The result comes back the same way as a JSON response. Knowing this chain helps me debug, because a failure can sit in my code, the server, the driver or the helper on the device."
Describing Appium as a tool that records taps, or not knowing there is a separate driver and an on-device helper behind the server.
Core plus drivers: the server is only the core; each platform driver is installed and updated on its own.
Plugins: add or change server behaviour, enabled when the server starts.
Migration traps: the default base path changed and non-standard capabilities need the appium: prefix.
"Appium 1 shipped with every driver bundled into the server. From Appium 2 the server is just the core, and drivers are installed separately, a bit like packages. I run appium driver install uiautomator2 or appium driver install xcuitest, and appium driver list shows what's there. The nice part is I can update the iOS driver without touching Android, and pin versions per project. Plugins are the other idea: they change server behaviour without a new driver. Image-based element finding, for example, moved into a plugin, and I switch plugins on when I start the server. Two things caught teams out when they migrated. The default base path changed from /wd/hub to just the root, so old server URLs stopped working. And any capability that isn't a W3C standard one needs the appium: prefix."
npm install -g appium
appium driver install uiautomator2
appium driver install xcuitest
appium driver list --installed
appium plugin install images
appium --use-plugins=images
Not knowing drivers are installed separately, so you cannot explain why a fresh install cannot start an Android session.
UiAutomator2: black box; works on any installed build and can reach system UI and other apps.
Espresso: grey box; runs inside the app's process and syncs with the UI thread, so fewer waits.
Trade-off: Espresso needs a server built and signed to match the app, and mostly sees only that app.
"UiAutomator2 is black box. It drives the phone the way a user would, from outside the app, so it works on any build you can install, including a release build, and it can reach things outside the app like the notification shade, system settings or a permission dialog. That makes it the default for most teams. Espresso is grey box. The Espresso driver builds a server that runs inside the app's own process, so it knows when the main thread is idle and waits automatically. Tests tend to be faster and less flaky, and it can reach app internals. The cost is setup: that server has to be built to match the app and signed with the same key, and it's mostly limited to what's inside the app. I'd pick Espresso when the team owns the app build and speed matters, UiAutomator2 for end-to-end flows across system screens."
Saying the two drivers are interchangeable, or that Espresso is simply faster with no setup cost or limits.
What it is: an XCTest-based app the XCUITest driver builds and installs; it runs a small server that drives the UI.
Why first runs fail: signing with a team and provisioning profile, the developer not trusted on the phone, Developer Mode off, slow first build.
Fixes: set signing capabilities, trust the profile, prebuild WDA and reuse it.
"WebDriverAgent is the helper the XCUITest driver puts on the device. It's an XCTest runner app, built with xcodebuild, and it starts a small HTTP server that receives commands and drives the UI through Apple's XCTest APIs. On a simulator that just works. On a real iPhone the first run usually fails for a few reasons. WDA has to be signed with an Apple developer team and a provisioning profile that includes the device, so I pass the team and signing identity in the capabilities, and sometimes a unique bundle ID for WDA. On the phone, depending on the account type, I may need to trust the developer in Settings, and on newer iOS versions Developer Mode must be switched on. And the first build is slow enough to hit launch timeouts. Once it works, I prebuild WDA and reuse it, so later sessions start in seconds instead of rebuilding."
Not knowing that WDA exists or that real-device iOS runs need code signing, and blaming Appium itself for every iOS session failure.
iOS: the XCUITest driver needs macOS with Xcode; simulators only run on a Mac.
Android: any major OS with the Android SDK, adb and a Java runtime.
Workarounds: tests can live anywhere and talk to a remote Appium server on a Mac, or to a cloud device farm.
"Not locally. The XCUITest driver needs macOS with Xcode installed, because WebDriverAgent is built with Xcode's tools and iOS simulators only run on a Mac. Android is more relaxed: Windows, Mac or Linux all work, as long as the Android SDK is installed, adb can see the device, and the environment variables for the SDK and Java are set. But the test code itself doesn't have to run on a Mac. My tests only send HTTP requests, so I can write and run them from Windows and point them at an Appium server on a Mac in the office or in CI, or at a cloud device farm that provides iOS devices. When setting up a new machine I run the driver's doctor check, which lists anything missing before I waste time on a failing session."
Claiming iOS automation works on Windows with the right plugin, or not knowing that simulators need macOS.
Must have: platformName and automationName pick the driver; udid picks the device when several are connected.
App: an app path, or package and activity for an installed app.
Prefix: W3C only defines a few standard capabilities; everything else needs a vendor prefix like appium:.
"At minimum I need platformName set to Android and automationName set to UiAutomator2, because together they tell the server which driver to use. On Android, appium:deviceName is mostly a label. What really picks the device when more than one is connected is appium:udid, the serial that adb devices prints. Then the app, either appium:app pointing to an APK, or appPackage and appActivity for an app that's already installed. I usually add appium:newCommandTimeout so a long debug pause doesn't end the session. The prefix is because Appium follows the W3C WebDriver standard, which only defines a handful of capabilities, like platformName and browserName. Anything vendor-specific must carry a prefix, so Appium's own ones are written appium:something. In Java I use the UiAutomator2Options class, which sets platform and driver for me and adds the prefixes automatically."
UiAutomator2Options options = new UiAutomator2Options()
.setDeviceName("Pixel_7_API_34")
.setUdid("emulator-5554")
.setApp("/builds/app-debug.apk")
.setNewCommandTimeout(Duration.ofSeconds(120));
AndroidDriver driver = new AndroidDriver(
new URL("http://127.0.0.1:4723"), options);
Listing capabilities without knowing which one selects the driver, or thinking deviceName alone reliably picks a device when several are connected.
noReset: keeps the app and its data between sessions; fast but state leaks.
fullReset: uninstalls and reinstalls; clean but slow.
On purpose: set state explicitly per test, and create it through APIs or deep links rather than the UI.
"With noReset set to true, Appium leaves the app and its data alone between sessions, so if the last test logged in, the next one starts logged in. It's fast, but tests start depending on each other. With fullReset set to true, the app is uninstalled and installed fresh, which gives a clean slate but costs time on every session. If you set neither, the driver does a middle-ground reset that differs between Android and iOS, so I don't rely on the default. What I do is decide state explicitly. I install fresh once per suite, then between tests I clear the app's data or terminate and relaunch it, depending on what the test needs. And for setup like being logged in with items in a cart, I create that through a backend API or a deep link, not by tapping through five screens each time."
Turning on noReset for speed with no plan for the state it leaves behind, then blaming Appium for random failures.
App file: Appium installs the APK, IPA or .app from a path or URL before the session.
Already installed: appPackage and appActivity on Android, bundleId on iOS, just launch it.
iOS builds: a simulator needs a simulator build; a real device needs a build signed for that device.
"I pass appium:app when I want Appium to install the build for me, which is the normal case in CI: it points to an APK for Android, or on iOS a .app for a simulator or an IPA for a real device, and it can be a local path or a URL. When the app is already on the device, say installed by the team or from a store for a smoke test, I skip the file and tell Appium what to launch: appPackage and appActivity on Android, and bundleId on iOS. To find the package and activity I open the app and ask adb which window has focus. One thing that trips people up on iOS: a build made for the simulator won't install on a real phone, and a real-device build has to be signed for that device."
Trying to install a simulator build on a real iPhone, or not knowing how to launch an app that is already installed.
Android: grant permissions automatically at install with a capability, or grant them through adb.
iOS: auto-accept or auto-dismiss alerts with a capability, or accept the alert in code.
Still test it: keep a dedicated test that sees and answers the dialog on purpose.
"For most tests the dialog is noise, so I remove it up front. On Android I set appium:autoGrantPermissions, which grants the runtime permissions the app declares when Appium installs it, or I grant specific ones with adb before the test. On iOS I can set appium:autoAcceptAlerts or autoDismissAlerts, and the XCUITest driver can also accept a system alert directly from code, the same way you'd accept a browser alert. But I don't want to hide the dialog everywhere, because the permission flow is a real feature. So I keep a few dedicated tests with permissions not granted: one that allows and checks the feature works, and one that denies and checks the app degrades gracefully, like showing a message instead of crashing."
Tapping Allow by coordinates in every test, or auto-granting everything and never testing what happens when a user says no.
Shared: accessibility id, id, class name, XPath.
Platform native: UiAutomator selectors on Android; predicate strings and class chains on iOS.
Preference: accessibility id or resource-id first, native queries next, XPath last.
"Appium supports accessibility id, id, class name and XPath on both platforms, plus native strategies: the UiAutomator selector on Android, and predicate strings and class chains on iOS. Image matching is available through a plugin. My first choice is accessibility id, because it works across both platforms: on Android it matches content-desc, on iOS the element's name, which usually comes from the accessibility identifier the developers set. On Android I'm also happy with a stable resource-id. When I need to match on more than one attribute, I use the native strategies, a UiSelector on Android or a predicate or class chain on iOS, because the platform evaluates them directly. XPath is my last resort. It's slow on mobile and it breaks when the layout shifts. And where the app has no good IDs, I ask the developers to add them rather than building clever XPaths."
Using XPath with indexes as the default strategy, or not knowing any locator that is specific to Android or iOS.
Slow: the driver must dump the whole UI tree as XML before it can run the query.
Fragile: paths tie the test to layout, which differs across devices and OS versions.
Instead: accessibility id, resource-id, UiSelector, predicate strings, class chains.
"Mobile platforms don't have an XPath engine built in. To answer an XPath query, the driver has to take a snapshot of the whole UI hierarchy, turn it into XML, and then search that XML. On a big screen with long lists, especially on iOS where building that snapshot is expensive, one lookup can take seconds, and in a suite that adds up fast. It's fragile because a path like the third child of the second layout depends on structure, and structure changes with screen size, OS version and small design tweaks. The alternatives are strategies the platform resolves natively: accessibility id, resource-id on Android, a UiSelector when I need text or other attributes, and on iOS a predicate string or class chain. If I really need XPath, I keep it short and anchored on a stable attribute, never on indexes."
Saying XPath is slow on mobile just because it is XPath, without knowing the driver must build the full page source to evaluate it.
Appium Inspector: start a session, see a screenshot and the element tree, and try locators live.
From code: print the page source when a test fails.
Webviews: use the browser's remote inspector for the web part.
"My main tool is Appium Inspector. I start the Appium server, give Inspector the same capabilities my test uses, and it opens a session showing a screenshot next to the element tree. I click an element and see its attributes: resource-id, content-desc, text and class on Android, or name, label, value and type on iOS. It suggests locators, and I can try a locator right there to see if it matches exactly one element before I put it in code. From the test itself, I can call getPageSource to dump the same XML, which I attach to failure reports. For the web part of a hybrid app, the native tree isn't enough, so I use Chrome's remote inspect page for an Android webview or Safari's Web Inspector for an iOS one, and pick CSS selectors there."
Copying the first locator a tool suggests without checking it is unique and stable.
Compose: test tags only show up as resource-ids when the app enables that in its semantics.
SwiftUI: accessibility identifiers become the element name, and screen readers do not speak them.
Agreement: a naming convention shared by both platforms so one locator works on each.
"Compose doesn't build classic Android views, so the resource-ids we relied on aren't there by default. Developers can give elements a test tag, but UiAutomator only sees test tags as resource-ids when the app switches on a semantics flag called testTagsAsResourceId, usually once near the root of the screen. So I ask for two things: turn that flag on, and add test tags to the elements we automate. I'd avoid asking them to put IDs into content descriptions, because TalkBack reads those aloud to real users. On SwiftUI the answer is the accessibility identifier modifier. It becomes the element's name, which Appium's accessibility id strategy matches, and VoiceOver doesn't read it. Best of all, I agree a naming convention with both teams, like login_button on each platform, so one accessibility id locator works on Android and iOS."
Asking developers to stuff test IDs into accessibility labels that screen readers speak, or settling for XPath on visible text across every screen.
Predicate string: match on attributes with conditions like equals, contains or begins with.
Class chain: a path through element types, with filters and indexes, like a light XPath.
Why: both are resolved by XCTest directly, so they are much faster than XPath.
"Both are iOS-only strategies that XCTest evaluates natively, which is why they're so much faster than XPath. A predicate string matches elements by their attributes, like type, name, label or value, with conditions such as equals, contains or begins with, and I can join conditions with AND or OR. I use it when an element is best described by what it is, like a button whose label starts with Pay. A class chain describes a path through the hierarchy, like a light XPath: find a cell whose label contains Order, then the first button inside it. I use that when I need structure, for example a button inside a particular cell in a list. The filter in backticks inside a class chain is itself a predicate, so the two work together. One detail worth knowing: class chain indexes start at 1, not 0."
WebElement pay = driver.findElement(AppiumBy.iOSNsPredicateString(
"type == 'XCUIElementTypeButton' AND label BEGINSWITH 'Pay'"));
WebElement orderButton = driver.findElement(AppiumBy.iOSClassChain(
"**/XCUIElementTypeCell[`label CONTAINS 'Order'`]/XCUIElementTypeButton[1]"));
Knowing only XPath for iOS, or mixing up predicate syntax with XPath syntax.
W3C Actions: a touch pointer that moves, presses, moves again over a duration, then lifts.
Driver commands: mobile: gesture commands for common swipes, scrolls and pinches.
Reuse: wrap it in one helper that works in screen proportions, not fixed pixels.
"TouchAction and MultiTouchAction were deprecated and then removed, so I use W3C Actions. I create a pointer input of kind touch and build a sequence: move to the start point, press down, move to the end point over a duration, then lift. The duration matters: a fast move is a fling, a slow one is a drag, and too short can be ignored by the app. Then I call perform. I calculate the start and end points from the screen or element size, like from four fifths of the way down to one fifth, so it works on any device. The other option is the driver's own commands through executeScript. The Android driver has mobile: swipeGesture and scrollGesture, and the iOS driver has mobile: swipe and scroll. Those are shorter and often more reliable, but they're platform-specific, so my helper picks one per platform."
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence swipe = new Sequence(finger, 1);
swipe.addAction(finger.createPointerMove(Duration.ZERO,
PointerInput.Origin.viewport(), startX, startY));
swipe.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()));
swipe.addAction(finger.createPointerMove(Duration.ofMillis(600),
PointerInput.Origin.viewport(), endX, endY));
swipe.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
driver.perform(List.of(swipe));
Hard-coding pixel coordinates from one phone, or still reaching for TouchAction without knowing it has been removed.
Android: UiScrollable through the UiAutomator strategy scrolls until the target appears.
iOS: the driver's mobile: scroll command toward a direction or a named element.
Fallback: swipe, check, and stop when the screen stops changing.
"On Android, lists recycle their rows, so an item far down the list often isn't in the element tree at all until it's close to the screen. The cleanest fix is UiScrollable through the UiAutomator locator: I describe the scrollable container and the target, and UiAutomator scrolls on the device until it finds it, all in one call. On iOS, the XCUITest driver has a mobile: scroll command that can scroll a container in a direction, or toward a child element matched by name or predicate. When neither fits, for example a custom scroll view, I write a small helper: swipe, look for the element, and repeat, but stop after a set number of tries or when the page source stops changing, so it can't loop forever at the bottom of the list."
WebElement settings = driver.findElement(AppiumBy.androidUIAutomator(
"new UiScrollable(new UiSelector().scrollable(true))"
+ ".scrollIntoView(new UiSelector().text(\"Settings\"))"));
settings.click();
Scrolling a fixed number of times with sleeps in between and hoping the element is there.
Check: ask the driver whether the keyboard is shown.
Dismiss: hide it, or press the keyboard's own Done or Return key the way a user would.
Or scroll: bring the button into view if the design keeps the keyboard open.
"First I confirm that's really the problem: the driver can tell me whether the keyboard is shown, and a screenshot at failure usually makes it obvious. Then I dismiss it in a way that matches what a user does. hideKeyboard works well on Android. On iOS it depends on which keys the keyboard has, so often the most reliable way is tapping the keyboard's Done or Return key, or tapping a neutral area of the screen. If the design keeps the keyboard open, I scroll the form so the button is visible instead. I put this in the page object's typing helper so every test gets it for free. And if a real user can't reach the button without closing the keyboard on small screens, that's worth raising as a usability bug too."
Tapping fixed coordinates where the button usually is, or adding a sleep and hoping the keyboard goes away.
Contexts: NATIVE_APP plus one WEBVIEW context per inspectable web view.
Switch: wait for the webview context, switch, then use normal web locators like CSS.
Back: switch to NATIVE_APP for native screens, dialogs and gestures.
"A hybrid app exposes more than one context. NATIVE_APP is the normal one, and each inspectable web view shows up as a context whose name starts with WEBVIEW. When I get to the web screen, I first wait until the webview context appears, because it only shows once the page has loaded. Then I switch to it. From that point Appium sends my commands to a web automation backend, chromedriver on Android or the WebKit remote debugger on iOS, so I use CSS selectors like on a website. When the flow goes back to a native screen, or a native dialog pops up, I switch back to NATIVE_APP. I keep the switching inside the page objects, so tests never need to know which part of the screen is web and which is native."
new WebDriverWait(driver, Duration.ofSeconds(15))
.until(d -> driver.getContextHandles().size() > 1);
for (String ctx : driver.getContextHandles()) {
if (ctx.startsWith("WEBVIEW")) {
driver.context(ctx);
break;
}
}
driver.findElement(By.cssSelector("#pay-now")).click();
driver.context("NATIVE_APP");
Trying to find web elements with native locators without switching context, then calling the screen untestable.
Not inspectable: the app build must allow web view debugging on Android and inspection on iOS.
Not a web view: some in-app pages are really the system browser, like a custom tab.
Timing and tooling: the page has not loaded yet, or the browser driver version does not match.
"I'd check a few things in order. First, is the web view inspectable at all? On Android, the app has to turn on web contents debugging, usually only in debug or test builds, so a release build shows nothing. On recent iOS versions the app also has to mark its web view as inspectable, and on a real device Web Inspector needs to be enabled in Safari's settings. Second, is it really a web view? Many apps open pages in a Chrome custom tab, which is the Chrome app on top of yours, not your app's web view. Third, timing: the context only appears after the page loads, so I poll for it. And if the context is listed but switching fails, on Android that's usually a chromedriver that doesn't match the web view's Chrome version, which I fix by pointing Appium at the right one."
Assuming it is an Appium bug and reinstalling everything, without checking whether the build allows web view debugging.
Virtual devices: fast, cheap to reset and parallel; right for every pull request.
Real devices: real performance, hardware, maker skins and networks; right before release.
Cloud farm: breadth without owning devices, at the cost of speed, queues and data review.
"Emulators and simulators are my first line. They start clean, run in parallel on CI machines, and are perfect for functional checks on every pull request. But they aren't real phones. Performance is different, hardware like the camera or real biometrics is faked or missing, and they don't show the changes phone makers add to Android, like aggressive battery savers that kill background apps. So before release I run on real devices, especially the lowest-end phone we support. A cloud farm gives me breadth, dozens of models and OS versions I don't own, but sessions are slower, there can be queues, and security has to approve sending test data there. I choose the device list from analytics: the most used OS versions and makers among our users, plus the smallest screen and the weakest device."
Picking one extreme, only emulators or only a farm of every device, without tying the choice to where users are or what each option cannot catch.
Device: a unique udid per session.
Ports: a unique systemPort on Android, and wdaLocalPort on iOS; others for webviews and streaming.
Test side: one driver per thread, and test data that devices do not share.
"Each session needs its own device, so I pass a unique udid, never just a device name. Then the ports. On Android, UiAutomator2 forwards a local port to its helper on the device, the systemPort, so each session needs a different one or they fight over it. If the tests touch webviews, the chromedriver port must differ too. On iOS the same idea applies to wdaLocalPort. I can run one Appium server with several sessions, or one server per device, which isolates logs nicely. On the test side, the driver lives in a ThreadLocal so parallel threads never share one, and my runner hands each thread a device from a pool. Finally the data: if four devices log in with the same account and change the same cart, tests fail for reasons that have nothing to do with the app, so each device gets its own account."
private static final ThreadLocal<AndroidDriver> DRIVER = new ThreadLocal<>();
static void start(String udid, int systemPort) throws MalformedURLException {
UiAutomator2Options opts = new UiAutomator2Options()
.setUdid(udid)
.setSystemPort(systemPort)
.setApp("/builds/app-debug.apk");
DRIVER.set(new AndroidDriver(new URL("http://127.0.0.1:4723"), opts));
}
static AndroidDriver driver() { return DRIVER.get(); }
Keeping the driver in a static field shared by all threads, or not knowing that ports like systemPort must differ per session.
Driver factory: config picks platform, device and capabilities; tests never build drivers.
Screen objects: one class per screen with a locator per platform, or a shared interface with two implementations.
Shared IDs: agree matching accessibility ids so most locators are the same on both.
Escape hatch: platform-specific steps only where the UX really differs.
"I'd split it into layers. Tests describe the user flow and never touch the driver. Below them are screen objects, one per screen, exposing actions like logIn. Below that is a gesture and wait helper layer, and at the bottom a driver factory that reads config, platform, device and capabilities, and builds the right driver. For locators, the Java client lets me annotate each field with an Android locator and an iOS locator, so one screen class serves both. Where a screen behaves differently, for example a date picker, I use an interface with an Android and an iOS implementation. The biggest win is agreeing with developers on matching accessibility ids across both apps, so most fields have the same locator. Test data comes from APIs, and reports record platform, device and app build so a failure is easy to place."
public class LoginScreen {
@AndroidFindBy(accessibility = "login_email")
@iOSXCUITFindBy(accessibility = "login_email")
private WebElement email;
@AndroidFindBy(id = "com.example.shop:id/login")
@iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeButton' AND name == 'Log in'")
private WebElement loginButton;
public LoginScreen(AppiumDriver driver) {
PageFactory.initElements(
new AppiumFieldDecorator(driver, Duration.ofSeconds(10)), this);
}
}
Two completely separate suites with copied tests, or one suite full of if-Android-else-iOS checks inside the tests themselves.
Device noise: animations, system pop-ups, keyboards, low storage and leftover app state.
Timing: async loading, slow CI emulators and fixed sleeps.
Infrastructure: the on-device helper crashing, devices dropping off, shared test data.
Method: classify failures from evidence, fix the cause, quarantine what you cannot fix yet.
"There's simply more between my code and the screen. First, device noise: animations, system pop-ups like OS updates or low battery, the keyboard covering elements, and state left over from the last test. I turn animations off on test devices, pre-grant permissions and reset app state on purpose. Second, timing: screens load data asynchronously, and emulators on busy CI machines are slow, so I use explicit waits on the condition I need, never fixed sleeps, and give emulators hardware acceleration. Third, infrastructure: the UiAutomator2 helper or WebDriverAgent can crash, devices disconnect, and parallel devices can share an account. Those show up as session errors, not assertion failures, so I track them separately. The method matters most: I tag every failure by cause from the logs and recordings, fix the biggest bucket first, and quarantine a test with a ticket rather than letting it cry wolf."
Answering only with automatic retries and longer sleeps, which hides both real bugs and the actual causes.
Screen: screenshot and page source at the moment of failure, plus a screen recording.
Logs: device logs such as logcat or the iOS system log, and the Appium server log for that session.
Context: device model, OS version, app build and capabilities.
"At the moment of failure I capture a screenshot and the page source, so I can see both what the user saw and what the element tree looked like. I also record the screen for each test and keep the recording only if it fails; a video shows things a single screenshot misses, like a pop-up that came and went. Then logs: logcat on Android or the system log on iOS, which is where crashes and stack traces show up, and the Appium server log for that session, which shows exactly which command failed and why. Last, the context: device model, OS version, app build number and the capabilities used. All of it is attached to the test in the report. With that, most failures can be classified in a couple of minutes without rerunning anything."
Only taking a screenshot, or saying you would rerun the test locally to see what happened.
Situation: which test, which devices, and how the failure looked.
Investigation: what evidence you compared between passing and failing devices.
Outcome: the fix in the test or the app, and what you changed so it would not happen again.
"At my last company, our checkout test passed on every emulator and most phones, but failed on our smallest real device every time. The log said the Place order tap had no effect. I compared recordings from a passing and a failing device, and on the small screen the button sat just below the fold when the address form was open, so our tap hit the wrong spot. The test fix was easy: scroll the element into view before tapping, inside the page object. But I also checked it as a user, and on that phone you genuinely couldn't see the button without closing the keyboard first, which many people wouldn't think to do. I raised that as a real bug, and the design was changed. Afterwards we added the smallest supported screen to the pull request device set."
A story that ends with adding a retry or excluding the device, with no cause found.
Starting point: what state the suite was in and how the team treated its results.
Changes: locators, waits, setup, device and data changes you made, in order of impact.
Result: how you knew trust improved, such as fewer reruns or failures acted on.
"In my last role I inherited an Appium suite that people ignored. It was mostly long XPaths with fixed sleeps, every test logged in through the UI, and a red run meant nothing because half of it failed at random. I started by tagging a month of failures by cause, which showed most were locators and timing. I agreed accessibility ids with both app teams and replaced the worst XPaths first, swapped sleeps for explicit waits, and moved login and test data setup to backend APIs. I also split a fast smoke set for pull requests from the full nightly run, and quarantined unstable tests with a ticket and an owner. After a couple of months a red run nearly always meant a real problem, and the proof was that developers started opening the reports themselves instead of asking me whether it was the tests again."
A rewrite-everything story with no data on what was failing and no sign that the team's behaviour changed.
The bug: what failed and on which device or OS version.
Why automation found it: coverage, repetition or conditions people rarely test by hand.
What followed: the fix and any change to how the team tests.
"In a previous project our nightly Appium run installed the app fresh on an emulator with the oldest Android version we still supported. One morning every test on that emulator failed at launch. The logcat we attach to failures showed a crash on start-up: a new feature called an Android API that doesn't exist on older versions, without checking the version first. Nobody had caught it by hand because the whole team used recent phones, and our manual checks upgraded an existing install rather than doing a fresh one. The developer added the version check the same day, before release. It changed two things for us. We kept the oldest supported OS in the nightly device list for good, and we added a fresh-install launch check to the pull request pipeline, since it takes under a minute."
Describing a bug any single manual pass would have caught, without explaining what the automation did differently.
Now: use the most stable locators available and keep them inside screen objects.
Contain the risk: avoid index-based paths and long text; note which locators are weak.
Next: a small, ranked request for ids on the screens tests touch most, scheduled after release.
"I wouldn't block on it, but I'd be careful how I build. For now I'd use the most stable thing each screen offers: a resource-id where one exists, content descriptions that are already there, short native queries on stable attributes, and XPath only anchored on something that won't move, never on indexes. Every locator lives in a screen object, so when ids arrive later I change one line, not fifty tests. I'd mark the weak locators in the code so the risk is visible. Then I'd make the request easy to say yes to: a short list of the exact elements on the screens our tests hit most, with suggested names that work for Android and iOS, ready to be done in the sprint after release. Adding an identifier is a tiny change per element, and showing a list of fifteen is much easier to agree to than asking for all of them."
Refusing to automate until the app is perfect, or silently building on index-based XPaths with no plan to replace them.
Data: pick devices from real user analytics plus the risky edges.
Split: full suite on emulators, smoke and key flows on real devices, long tail rotated.
Agree: write down what is not covered and get the team to accept it.
"I'd start from data, not a wish list. Analytics tell me which OS versions, phone makers and screen sizes our users actually have, so I'd cover the ones that make up most of our users, then add the risky edges: the oldest OS we support, the smallest screen and the weakest device. That might be six or seven phones. The full regression suite runs on emulators and simulators, which are free to parallelise. On the real devices I run the smoke set and the flows that matter most for money and sign-in, where hardware and maker quirks bite. The rest of the twenty I rotate, a few per release, so over a month every one gets covered. And I'd write this down and share it, including what we're not testing on each release, so the risk is a team decision and not a surprise."
Picking devices by what the office happens to have, or quietly testing fewer than promised without telling anyone.
Split: a short, high-value set per pull request; the full run nightly or before release.
Faster tests: deep links and API setup instead of tapping through, no reinstall per test, no XPath or sleeps.
Faster runs: parallel emulators, prebuilt helpers, sharding by measured test time.
"First I'd accept the developers' behaviour as a signal: the suite isn't useful at ninety minutes. I'd pick a pull request set that finishes in about ten, covering sign-in, the main purchase flow and whatever the changed area touches, and move the rest to nightly and pre-release runs. Then I'd make each test cheaper. Most mobile time goes on setup, so I'd log in and create data through APIs, jump straight to screens with deep links, and stop reinstalling the app for every test. I'd remove fixed sleeps and slow XPath locators, which quietly add minutes. Then parallelise: several emulators with their own ports, tests sharded by their measured duration, and prebuilt helpers so sessions start quickly. Finally I'd delete or merge tests that have never caught anything. I'd share before and after timings so the team sees it's worth waiting for again."
Only asking for more devices, or making the pipeline optional, without making the tests themselves cheaper.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.