Mobile Test Automation Explained

Mobile test automation drives an app on an emulator, simulator or real device and checks its behaviour without a person tapping the screen. Three tools cover most of the field: Espresso for Android and XCUITest for iOS, which are the platforms’ own UI testing frameworks, and Appium, an open-source server that drives both platforms through the WebDriver protocol. They differ in where the test runs, how it waits for the app, who usually writes it, and how much of the device it can reach.

This explainer compares the three, shows a minimal test in each, and covers the parts that decide whether a mobile suite is trusted or ignored: locators, flakiness, device farms and CI. The wider context — device strategy, risk areas, security — is in the mobile testing pillar.

What to automate on mobile

The test pyramid applies to mobile as to any product (the guide on manual vs automated testing covers why): most checks belong below the UI, where they are fast and stable. On Android, local unit tests run on the development machine’s JVM in seconds, while instrumented tests run on a device or emulator; on iOS, XCTest runs unit tests and UI tests from the same Xcode project. Business rules, validation and calculations belong in unit tests and in API tests against the back end. UI tests are for the journeys that must never break — sign-in, onboarding, search, checkout, the app’s core feature — and for the wiring between screens.

  • Unit tests — view models, formatters, validation, local database queries; run on every commit.
  • Instrumented or integration tests — code that needs the platform: storage, the local database, background work; run on emulators or simulators.
  • API tests — the back end the app calls, including error responses and old-client compatibility.
  • UI tests — a small set of critical journeys per platform, run in CI on emulators and on real devices before release.
  • Manual and exploratory testing — gestures, real interruptions, look and feel, real-device behaviour (see the section on what stays manual).

Espresso, XCUITest and Appium compared

There is no universal winner. If developers own the tests and the platforms are built by separate teams, the native frameworks fit best: fastest feedback, least flakiness, same language as the app. If a separate automation team covers both platforms with shared flows, or tests must run against release-like builds without source access, Appium’s single codebase and language choice matter more. Many teams combine them: native frameworks for screen-level tests in the developer pipeline, and a small cross-platform Appium suite for end-to-end journeys.

  • Espresso — Android only; tests in Kotlin or Java; runs inside the app’s process as an instrumentation test. It synchronises automatically with the app’s UI thread, so it acts only when the UI is idle; background work it cannot see is registered through idling resources. Fast and stable; usually written by Android developers alongside the code. It cannot reach system UI outside the app — UI Automator covers that.
  • XCUITest — iOS only; tests in Swift or Objective-C, written in Xcode with XCTest (Apple now documents the UI testing API as XCUIAutomation). The test runner is a separate process that drives the app through the accessibility layer, as a user would. It waits for the app to settle before interacting and can handle system alerts. Usually written by iOS developers or automation engineers who work in Xcode.
  • Appium — Android, iOS and other platforms; tests in any language with a WebDriver client (Java, Python, JavaScript, C#, Ruby…). A client sends W3C WebDriver commands to the Appium server, which uses a platform driver — for example the UiAutomator2 driver on Android and the XCUITest driver on iOS — to act on the device. It treats the app as a black box, can switch into WebView contexts of hybrid apps, and can automate mobile browsers. Each step crosses more layers, so it is slower than the native frameworks.
Where the test code runs explains the rest: Espresso runs inside the app’s process, XCUITest drives the app from a separate process, and each Appium step crosses a client, a server and a driver.

EspressoAndroid only

Inside the app’s process

  1. 1Test: find a view, act, assertAn instrumentation testwaits for an idle UI thread
  2. 2The app’s views
Test language:
Kotlin or Java
How it waits:
Automatically for an idle UI thread; background work it cannot see is registered as idling resources
Usually written by:
Android developers, alongside the code
Reach:
Only inside the app; system UI needs UI Automator
Speed:
Fast and stable

XCUITestiOS only

  1. 1Test runnerA separate processthrough the accessibility layer
  2. 2The appIts element tree, queried by type and identifier
Test language:
Swift or Objective-C
How it waits:
Waits for the app to settle; waitForExistence(timeout:) for elements that appear later
Usually written by:
iOS developers or automation engineers in Xcode
Reach:
The app, plus system alerts
Speed:
Native: faster feedback than Appium

AppiumAndroid, iOS and others

  1. 1Test code + WebDriver clientW3C WebDriver commands
  2. 2Appium server
  3. 3Platform driverFor example, UiAutomator2 on Android, the XCUITest driver on iOS
  4. 4The app on the deviceTreated as a black box
Test language:
Any language with a WebDriver client
How it waits:
Explicit waits for a condition, such as the element being present
Usually written by:
Fits a separate automation team covering both platforms
Reach:
The app as a black box, WebView contexts of hybrid apps, mobile browsers
Speed:
Slower: each step crosses more layers

Espresso: in-process and synchronised

An Espresso test finds a view with a matcher, performs an action on it, and checks an assertion. Before every action and assertion Espresso waits until the UI thread’s message queue is idle, which removes most of the waiting code other tools need. Work that runs elsewhere — a custom thread pool, a network library Espresso does not know — must be registered as an idling resource, or the test will race it. Screens built with Jetpack Compose are tested with Compose’s own testing APIs, which follow the same principle.

@RunWith(AndroidJUnit4::class)
class LoginTest {
    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun wrongPasswordShowsError() {
        onView(withId(R.id.email)).perform(typeText("layla.h@example.com"), closeSoftKeyboard())
        onView(withId(R.id.password)).perform(typeText("wrong-pass-1"), closeSoftKeyboard())
        onView(withId(R.id.sign_in)).perform(click())
        onView(withText(R.string.error_invalid_credentials)).check(matches(isDisplayed()))
    }
}

XCUITest: black-box through accessibility

An XCUITest test launches the app as a separate process and queries its element tree by type and identifier: buttons, text fields, static texts. Developers set an accessibilityIdentifier on each element the tests need; unlike the accessibility label, the identifier is not spoken by VoiceOver, so it can be a stable technical name. Launch arguments and environment variables put the app into a test state — a mock server URL, a reset data store, animations disabled — without special builds. Use waitForExistence(timeout:) for elements that appear after asynchronous work instead of fixed sleeps.

final class LoginUITests: XCTestCase {
    func testWrongPasswordShowsError() {
        let app = XCUIApplication()
        app.launchArguments += ["-uiTesting", "-resetState"]
        app.launch()

        let email = app.textFields["login.email"]
        email.tap()
        email.typeText("layla.h@example.com")

        let password = app.secureTextFields["login.password"]
        password.tap()
        password.typeText("wrong-pass-1")

        app.buttons["login.submit"].tap()

        XCTAssertTrue(app.staticTexts["login.error"].waitForExistence(timeout: 5))
    }
}

Appium: WebDriver for mobile

Appium is a server. Your test, written with a WebDriver client library, opens a session by sending capabilities — which platform, which automation driver, which app, which device — and then sends commands such as find element, click and send keys. Since Appium 2 the drivers are installed separately from the server, so a project states exactly which driver versions it uses. Capabilities that are not part of the W3C standard carry the appium: prefix. Because Appium drives the app from outside, the same test can run against a debug build on an emulator and a release candidate on a device-farm phone.

from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

caps = {
    "platformName": "Android",
    "appium:automationName": "UiAutomator2",
    "appium:deviceName": "Pixel_Test",
    "appium:app": "/builds/shop-4.0.0-debug.apk",
    "appium:autoGrantPermissions": True,
}
driver = webdriver.Remote("http://127.0.0.1:4723", options=UiAutomator2Options().load_capabilities(caps))
try:
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login.email").send_keys("layla.h@example.com")
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login.password").send_keys("wrong-pass-1")
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login.submit").click()
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((AppiumBy.ACCESSIBILITY_ID, "login.error")))
finally:
    driver.quit()

Locators on mobile

A locator is how a test finds an element, and on mobile the choice decides both stability and speed. Agree an identifier convention with developers (for example screen.element: login.submit), set identifiers on every element tests touch, and make "has test identifiers" part of the definition of done. The general principles are in the guide on locator strategies; the mobile specifics follow.

  • Accessibility id — Appium’s preferred strategy. On iOS it matches the accessibilityIdentifier, which VoiceOver does not read, so it can be a technical name. On Android it matches the content description, which TalkBack does read aloud — so on Android it must be a real human label, or you should use the resource id instead.
  • Resource id (Android) — for example com.example.shop:id/sign_in; stable as long as developers keep ids unchanged.
  • Platform-native queries — UiSelector for UiAutomator2, predicate strings and class chains on iOS — when the above do not fit; faster than XPath.
  • Visible text — breaks when the app is localized (Arabic and English builds show different text); use only with per-locale resources.
  • XPath — last resort: slow on mobile because the whole view hierarchy has to be serialised, and fragile when layouts change.
  • Cross-platform frameworks (React Native, Flutter) expose their own test-id property; check what it maps to on each platform before building locators on it.

Why mobile UI tests are flaky — and what to do

A flaky test passes and fails on the same code. Mobile adds its own causes to the usual ones (asynchrony, shared state, remote services, time), and a suite that fails randomly is soon ignored. The flaky tests guide covers detection and quarantine; these are the mobile-specific sources.

  • Animations and transitions — disable them on test devices and emulators (the three animation scale settings below), or through launch arguments on iOS.
  • Asynchrony the framework cannot see — register idling resources in Espresso, wait for conditions (element exists, is enabled) everywhere else; never use fixed sleeps.
  • System dialogs — permission prompts, "app isn’t responding", keyboard tips, OS update prompts. Pre-grant permissions where the test is not about them, and handle expected system alerts explicitly.
  • State leaking between tests — a signed-in session, local data, feature flags. Reset per test: clear app data (Android Test Orchestrator can clear package data between tests) or launch with a reset argument on iOS.
  • Real network and a shared back end — use a mock server or a seeded test environment, and generate unique data per test run.
  • The soft keyboard covering the next element, and elements that need scrolling into view.
  • Device and emulator health — slow boots, full storage, low battery or overheating farm devices. Add health checks; retry at infrastructure level only, never to hide a real failure.
  • Locale, timezone and clock differences between devices in the matrix.
# Android — turn off animations on a test device or emulator
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

Try it: Mock API Response Generator Try it: API Status Code Simulator Try it: Mock Error Response Generator Try it: Fake User Generator

Device farms and CI pipelines

Android emulators run on Linux, Windows or macOS CI machines and need hardware acceleration to be fast (KVM on Linux); iOS simulators require macOS. Split long suites into shards that run in parallel on several emulators or devices. A cloud device farm takes your app and test package, runs them on the real devices you select, and returns results with videos, screenshots and logs; the same devices are usually available for manual sessions. Choose farm devices from your device matrix, and use pairwise selection for the secondary parameters (language, orientation, font size) rather than multiplying runs. The tiers in the illustrative pipeline below are those of the example device matrix in the mobile testing pillar: Tiers 1 and 2 on every release, Tier 3 before major releases.

Publish results in a format the CI server understands (JUnit XML is the common one) and keep the artefacts of every failed test. A red build without a video and a device log is a build someone will re-run instead of fixing.

Example mobile pipeline

on every pull request
  1. unit tests (JVM / XCTest unit)
  2. build the app and the test package (test APK / UI test bundle)
  3. UI smoke: 10–20 critical tests on one Android emulator and one iOS simulator
  4. API contract tests against the staging back end
nightly
  5. full UI suite, sharded across emulators and simulators
  6. Tier 1 and Tier 2 real devices in a device farm
before release
  7. release candidate on the Tier 1 and Tier 2 devices (Tier 3 too before a major release)
     + exploratory session on real devices
  8. oldest supported app version against the new back end (API smoke)
for every failed test: video, screenshots, device log, JUnit XML

Try it: Pairwise Testing Generator Try it: API Schema Validator

What stays manual

Automation checks what you already know to ask. On mobile, several important questions are still answered best by a person with a real device: how gestures feel, whether a layout looks right on a device nobody scripted, what happens during a real phone call on a real network, and whether the app is usable with a screen reader. Automated accessibility checks exist on both platforms — Espresso can run accessibility checks during tests, and XCUITest can run an accessibility audit — and they catch missing labels, small touch targets and low contrast, but they do not replace a TalkBack or VoiceOver session.

  • Exploratory sessions on real devices for each release candidate.
  • Real interruptions: calls, Bluetooth, notifications from other apps.
  • Visual review across devices, unless you maintain screenshot tests.
  • Screen reader and large-text sessions.
  • Battery, heat and performance on the lowest-end device.
  • Store builds: installing from the store or the beta channel exactly as users do.

Try it: Test Data Factory

Frequently asked questions

Appium or Espresso — which should I use?

Espresso if Android developers write the tests and you want the fastest, most stable feedback. Appium if one automation team covers Android and iOS with shared flows, prefers one language, or must test release-like builds as a black box. Many teams use both.

Is XCUITest the same as XCTest?

XCTest is Apple’s testing framework for unit, performance and UI tests. XCUITest is the common name for its UI testing part, which Apple now documents as XCUIAutomation: tests that launch the app and interact with it through accessibility.

Can Appium test hybrid apps and mobile web?

Yes. Appium can switch from the native context into a WebView context of a hybrid app and use web locators there, and it can automate mobile browsers such as Chrome on Android and Safari on iOS.

Why use accessibility ids as locators?

They are independent of layout and visible text, so they survive redesigns and localization, and they are fast to look up. On Android, remember that the accessibility id is the content description TalkBack reads, so it must be a real label or you should use resource ids.

How do I run mobile UI tests in CI?

Run a small smoke suite on an Android emulator (with hardware acceleration) and an iOS simulator (on macOS runners) on every pull request, the full suite nightly in shards, and real devices in a device farm before release, keeping video and logs of failures.

How do I reduce flakiness in mobile UI tests?

Disable animations, rely on the framework’s synchronisation and condition waits instead of sleeps, reset app state per test, pre-grant permissions, mock or seed the back end, and keep devices healthy. Quarantine flaky tests and fix the cause.

References

  • Appium DocumentationAppium (OpenJS Foundation)Official documentation for automating native, hybrid and mobile-web apps on iOS and Android.
  • EspressoAndroid Developers (Google)Android's UI testing framework, which synchronises test actions with the app's UI thread automatically.
  • XCUIAutomationAppleApple's API for UI tests that launch an app and interact with its elements the way a user does (the basis of XCUITest).
  • XCTestAppleApple's framework for unit, performance and UI tests of Xcode projects on iOS and other Apple platforms.
  • Test apps on AndroidAndroid Developers (Google)Android's official testing guidance: test types and scopes, local vs instrumented tests, and testing best practices.
  • Eradicating Non-Determinism in Tests (Martin Fowler)martinfowler.com · 2011Why flaky tests destroy a suite's value, their common causes (isolation, asynchrony, remote services, time, resource leaks) and quarantine as a response.
  • Certified Tester Advanced Level Test Automation Engineering (CTAL-TAE) v2.0ISTQB · 2024ISTQB's advanced syllabus on designing, implementing and maintaining test automation solutions and their architecture.
  • Certified Tester Mobile Application Testing (CT-MAT)ISTQB · 2019ISTQB's specialist syllabus on mobile testing: app types, device and platform challenges, test types and tools.

Practice exercise

Stabilise a flaky Appium test for a promo code

Your task

You test a grocery delivery app (fictional) for Android and iOS with an Arabic and an English UI. Release 7.2 adds promo codes to the cart, and an automation engineer added the Appium test below to the nightly UI suite, which runs in four parallel shards on device-farm phones. Over the last 40 nights the test ran once a night and failed 14 of its 40 runs, the team has started to ignore the nightly results, and the release is close. The material gives the set-up, the test with line numbers, every failed run with what the farm’s video shows, and two notes from the team.

Work on paper with the guide’s sections on layers, locators, flakiness and device farms, and refer to the line numbers. Two people are waiting for your answer: Reem, the product owner, wants to know whether SAVE20 works, and Omar, a developer, proposes to re-run failed tests automatically up to three times. Allow 45–60 minutes.

Material
Grocery delivery app (fictional) - Android and iOS, Arabic and English UI
Release 7.2, Android build 7.2.0-rc1
Promo rule PR-4: the code SAVE20 takes 20% off the cart subtotal.

Nightly UI suite: pytest + Appium 2 with the UiAutomator2 driver. The suite is split into
4 shards that run in parallel, one per device-farm phone: Phone A, B, C and D.
Each night a shard plan puts every test on one shard, so a test runs once a night.
Each shard runs its share of tests/ui with plain pytest (no options); the only artefact
kept is the farm's video of the run.
Phone C is the low-end phone with the smallest screen in the matrix.
The pairwise device matrix gives every run a language: en or ar.
All shards sign in with the same test account: qa.shopper@example.com

test_checkout.py
 1  import time
 2  from appium import webdriver
 3  from appium.options.android import UiAutomator2Options
 4  from appium.webdriver.common.appiumby import AppiumBy
 5
 6  CAPS = {
 7      "platformName": "Android",
 8      "appium:automationName": "UiAutomator2",
 9      "appium:app": "/builds/grocer-7.2.0-rc1.apk",
10      "appium:noReset": True,
11  }
12
13  def test_apply_promo_code(device_name):
14      # precondition: test_login has already signed in as qa.shopper@example.com
15      # and the cart holds one item: rice 5 kg, SAR 100.00
16      caps = {**CAPS, "appium:deviceName": device_name}
17      driver = webdriver.Remote(
18          "http://127.0.0.1:4723",
19          options=UiAutomator2Options().load_capabilities(caps))
20      driver.find_element(AppiumBy.XPATH,
21          "//android.widget.FrameLayout[2]/android.widget.LinearLayout"
22          "/android.widget.TextView[3]").click()   # Cart tab
23      time.sleep(3)
24      promo = driver.find_element(AppiumBy.ID, "com.example.grocer:id/promo_input")
25      promo.send_keys("SAVE20")
26      driver.find_element(AppiumBy.XPATH,
27          '//android.widget.Button[@text="Apply"]').click()
28      time.sleep(2)
29      total = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "cart.total").text
30      assert total == "SAR 80.00"
31      driver.quit()

Last 40 nights: test_apply_promo_code ran 40 times, passed 26 times, failed 14 times.

F1-F5    the 5 runs with language ar (all four phones)
         NoSuchElementException: //android.widget.Button[@text="Apply"]
         video: the cart screen in Arabic; the button reads "تطبيق" (Apply),
         the total reads "100.00 ر.س"
F6-F8    Phone C, en (the other en runs on Phone C passed)
         AssertionError: assert 'SAR 100.00' == 'SAR 80.00'
         video: SAVE20 is in the field; the keyboard stays open and covers "Apply"
F9-F10   Phone B, en
         AssertionError: assert 'SAR 100.00' == 'SAR 80.00'
         video: a spinner on the total; it changes to SAR 80.00 after the test has ended
         staging log: on those two nights the promo request took 2.6 s and 3.1 s
F11-F12  Phone A, en
         AssertionError: assert 'SAR 140.00' == 'SAR 80.00'
         video: the cart holds 3 items, subtotal SAR 175.00; a test on another shard
         added 2 of them while this test ran
F13      Phone D, en
         NoSuchElementException: (the Cart tab XPath, lines 20-22)
         video: system dialog "Allow Grocer to send you notifications?"
F14      Phone B, en
         NoSuchElementException: (the Cart tab XPath, lines 20-22)
         video: the sign-in screen; that night the shard plan put test_login on Phone D

Team notes
  Reem (product owner): "Before we ship 7.2: does SAVE20 work or not?"
  Omar (developer): "It is just flaky. Let's re-run failed tests automatically up to
  3 times so the nightly turns green."
Steps
  1. Group the 14 failed runs. For each group, name its source from the guide (locator, asynchrony, soft keyboard, system dialog, state leaking between tests, shared back end), the lines of the test responsible, and whether it fails every time under a known condition or only some of the time. Then write one or two sentences for Reem.
  2. Review the test for problems that have not failed a run yet: locators, the assertion, clean-up and the evidence kept when a test fails.
  3. Write down what the stable version of the test does differently, as a numbered list or in Python.
  4. Split the checks between layers: which checks of PR-4 belong in unit or API tests, what stays in this UI test, and at least one check that stays manual.
  5. Reply to Omar, and say what happens to the test until it is fixed and where it runs afterwards.
Show the model answer
Model answer

The six groups. F1–F5: a locator defect in the test, not flakiness. Lines 26–27 look for the visible English text "Apply", which the Arabic UI shows as "تطبيق", so the lookup fails on every Arabic run and on no English one; it only looks random because the matrix gave Arabic to just 5 of the 40 runs, spread over all four phones. F6–F8: the soft keyboard covering the next element. After send_keys on line 25 the keyboard stays open and, on the smallest screen in the matrix, covers "Apply", so the tap on lines 26–27 does not reach it and the total stays at SAR 100.00; it happens only on Phone C, and not on every run there. F9–F10: asynchrony handled with a fixed sleep. Line 28 waits 2 seconds, the promo request took 2.6 and 3.1 seconds, and line 29 read the total before it changed.

F11–F12: shared state on a shared back end. All four shards sign in with one account, so a test on another shard added two items to the same server-side cart while this test ran (a subtotal of SAR 175.00 less 20% gives SAR 140.00); lines 14–15 assume a cart that the test never creates. F13: a system dialog. The notification prompt covers the app because permissions are not pre-granted (lines 6–11); it appears only on a run where no earlier test on that phone has answered it. F14: state leaking between tests, as a dependency on test order. The test relies on test_login having signed in on the same phone before it (line 10 and lines 14–15), and that night the shard plan put test_login on another phone.

Flaky or not: F1–F5 fail every time the language is Arabic, so they are a reproducible defect in the test. F6–F14 pass and fail on the same code depending on conditions the test does not control — the keyboard on the smallest screen, response time, what the other shards do, the device’s state and the shard plan — so they are flaky in the guide’s sense (even on Phone C the other English runs passed). For Reem: all 14 are false-positive results — the failures come from the test and its set-up, not from a defect in SAVE20. The evidence points the other way: the 26 passing English runs read SAR 80.00, and the F9–F12 videos show the discount applied (SAR 80.00 after the spinner, SAR 140.00 on a SAR 175.00 cart). What the nightly has not checked at all is the Arabic UI, because no Arabic run got past "Apply". So SAVE20 works in the English UI, on the one-item cart and on the three-item cart of F11–F12; the Arabic UI and the rest of PR-4 need checks that can be trusted (step 4), and until the test is fixed SAVE20 in Arabic has to be confirmed another way, for example with a manual check on the release candidate.

Problems that have not failed a run yet. Lines 20–22: an XPath built from the positions of views — slow on mobile, because the whole view hierarchy has to be serialised, and broken as soon as the tab bar changes; the Cart tab needs an identifier. Line 23: a second fixed sleep, which costs 3 seconds on every run and makes line 24 fail on a night the cart loads more slowly. Line 29: on Android an accessibility id is the content description, which TalkBack reads aloud, so a TalkBack user hears the technical name cart.total instead of the amount; report that as an accessibility defect in the app, and use a resource id in the test. Line 30: the expected value is the English string SAR 80.00, but the Arabic UI shows "ر.س" (see F1–F5), so the Arabic runs will fail here as soon as the locator is fixed. Line 31: driver.quit() is skipped whenever an earlier line raises — a failed lookup or the assertion — so the session stays open on the farm phone until Appium’s command timeout ends it. Nothing cleans up the data either: the test never removes SAVE20 or the item from the shared account’s server-side cart, so the next test that signs in with that account inherits whatever this one left. And the test keeps nothing of its own when it fails: no screenshot of the failing step, no page source, no device log and no JUnit XML for CI, only the farm’s video.

The stable version of the test. (1) It creates what it needs: a unique test user per run, a cart seeded through the test environment (one item at SAR 100.00) and a sign-in in the test’s own set-up. "appium:noReset": True is removed, so no session inherits app data from another test, and the test depends neither on test_login nor on the shard plan. (2) "appium:autoGrantPermissions": True pre-grants permissions, as in the guide’s example (the UiAutomator2 driver ignores it while noReset is true, one more reason to remove line 10); the notification prompt gets a test of its own. (3) Resource ids agreed with the developers, for example com.example.grocer:id/tab_cart, com.example.grocer:id/promo_apply and com.example.grocer:id/cart_total, found with AppiumBy.ID; on iOS the same elements get accessibilityIdentifier values such as cart.total, which VoiceOver does not read. The content description cart.total is removed from the Android app, so TalkBack reads the amount, and "has test identifiers" goes into the definition of done. (4) No time.sleep: WebDriverWait waits for conditions — the promo field is visible, "Apply" is clickable, the total shows the expected value (EC.text_to_be_present_in_element, with a timeout of about 10–15 seconds). (5) The keyboard is closed with driver.hide_keyboard() before the tap on "Apply". (6) The assertion compares the amount as a number, or the expected text for the run’s language. (7) driver.quit() moves into a finally block, as in the guide; on failure the test saves a screenshot, the page source and the device log, and CI receives JUnit XML (pytest --junitxml=results.xml). (8) The farm phones run with the three animation scale settings at 0.

Layers. The 20% calculation is a business rule, so its cases belong below the UI: unit tests of the discount calculation (20% of the subtotal, and rounding to 0.01 — PR-4 does not say how to round, so ask Reem) and API tests of the promo endpoint (a valid code, an unknown code and an empty code, with the expected response for each). They run in seconds on every pull request and need no device. The UI test keeps the wiring: on the cart screen, a valid code updates the total that is shown; a second UI test may check that an unknown code shows its error message. Both languages stay in the matrix, because the Arabic runs are what exposed the locator and assertion problems. Stays manual: a TalkBack session on the cart screen once the content description is fixed, and, on Phone C with a large font size, whether a user can reach "Apply" while the keyboard is open.

Reply to Omar. Automatic re-runs would turn the nightly green without fixing anything: F1–F5 would still fail on every Arabic run, and for the other failures a re-run hides the cause — and would hide a real intermittent defect in the promo service just as well. Retry only at infrastructure level (a farm phone that fails its health check, a session that cannot start), never to hide a failure. Until the fix, quarantine the test: it leaves the result that decides whether the nightly is green, keeps running, and is tracked with an owner and a date. After the fix, run it repeatedly across the whole matrix, both languages and Phone C included, before it returns to the quality gate. From then on it belongs in the nightly farm run and in the release-candidate run on the Tier 1 and Tier 2 phones, and as part of the checkout journey it can join the UI smoke suite that runs on one Android emulator for every pull request.

If your answer differs, check each point against two questions that sum up the guide’s advice: does the test create and own everything it depends on (account, cart, permissions, device settings), and does it wait for a condition rather than for time? A different fix that removes the same cause is fine — for example a mock server that returns the promo response in the UI test, while API tests cover the real endpoint.

Your progress on this topic

Part of the QA Roadmap

Tools mentioned in this guide

Generate complete mock API responses (status, headers, JSON body) for success, list, detail, error and empty cases.

Mocking

Get a public endpoint that returns any HTTP status code, with optional delay and custom JSON body, to test client error handling.

API Testing

Generate structured error responses (validation, auth, not found, rate limit, server error) in popular formats.

Mocking

Generate fake user records (name, username, email, password, avatar seed, role) for test accounts.

Test Data

One-stop factory for synthetic test data: users, customers, companies, addresses, numbers, dates and strings in JSON, CSV, SQL or XML.

Test Data

Validate API responses against a JSON Schema or an OpenAPI response schema.

API Testing

How Mutqan Soft can help

More guides

More guides