Article

Appium Tutorial: Mobile Automation Cheat Sheet (Appium 2.x, Python)

14 min read
Appium Tutorial Mobile Automation

This Appium tutorial doubles as a scannable cheat sheet for writing and running Appium tests. Every section is a table or a copy-paste snippet , skim to what you need. Examples use the Appium-Python-Client against Appium 2.x.

1. Appium in 30 Seconds

Appium is a client–server tool. Your test (the client) sends W3C WebDriver commands over HTTP to the Appium server, which forwards them to a platform driver that controls the device:

Python test  ──HTTP──▶  Appium server  ──▶  UiAutomator2 driver  ──▶  Android device
                                        └──▶  XCUITest driver     ──▶  iOS device

Appium 2.x note: drivers are no longer bundled , you install them separately as plugins, and the default server base path is now / (it was /wd/hub in 1.x). This is a common reason old tutorials fail when copied verbatim. See the Appium 2.0 migration guide for the full list of 1.x → 2.x changes.

2. Setup Quick-Start

# Install Appium 2.x
npm install -g appium

# Install the drivers you need (one-time)
appium driver install uiautomator2   # Android
appium driver install xcuitest       # iOS

# Verify environment (Android example)
appium driver doctor uiautomator2

# Start the server (defaults to http://127.0.0.1:4723)
appium

# Python client
pip install Appium-Python-Client

3. Desired Capabilities : Android vs iOS

One of the most-referenced parts of any cheat sheet. In Appium 2.x, pass these via typed Options objects. For the full list, see Kobiton’s guide to Appium desired capabilities explained.

CapabilityAndroidiOSPurpose
platformNameAndroidiOSTarget platform
automationNameUiAutomator2XCUITestDriver to use
deviceNamePixel 7iPhone 15Device label
platformVersion1417.2OS version
apppath/URL to .apkpath/URL to .ipa/.appBuild under test
App identityappPackage + appActivitybundleIdLaunch target
udidreal device serialreal device UDIDSpecific device
noReset / fullResetControl state between sessions
from appium.options.android import UiAutomator2Options

options = UiAutomator2Options()
options.platform_name = "Android"
options.device_name = "Pixel 7"
options.app = "/path/to/app.apk"
options.app_package = "com.example.app"
options.app_activity = ".MainActivity"
options.no_reset = True
options.automation_name = "UiAutomator2"
from appium.options.ios import XCUITestOptions

options = XCUITestOptions()
options.platform_name = "iOS"
options.device_name = "iPhone 15"
options.platform_version = "17.2"
options.bundle_id = "com.example.app"
options.automation_name = "XCUITest"

4. Start (and End) a Session

from appium import webdriver

# Appium 2.x default base path is "/", NOT "/wd/hub"
driver = webdriver.Remote("http://127.0.0.1:4723", options=options)

# ... your test steps ...

driver.quit()   # always release the session

Running on a device cloud? Point the same webdriver.Remote at a remote endpoint. You can also build caps from a dict with load_capabilities():

options = UiAutomator2Options().load_capabilities({
    "platformName": "Android",
    "appium:deviceName": "Galaxy S23",
    "appium:automationName": "UiAutomator2",
    # ...plus your cloud auth / session capabilities
})
creds = f"{os.environ['KOBITON_USER']}:{os.environ['KOBITON_API_KEY']}"  # never hard-code these
driver = webdriver.Remote(
    f"https://{creds}@api.kobiton.com/wd/hub", options=options)

Note: when you build caps as a plain dict, non-standard keys need the appium: prefix (appium:automationName) , the typed Options classes add it for you.

5. Appium Locator Strategies

Ranked by reliability. Prefer the top of the table; treat XPath as a last resort. For a deeper walkthrough, see Appium element locator strategies.

StrategyAppiumBy constantPlatformNotes
Accessibility IDACCESSIBILITY_IDBothGenerally the most reliable — stable, cross-platform
ID / resource-idIDAndroidFast and stable when devs add IDs
Class nameCLASS_NAMEBothCoarse; returns many matches
UiAutomatorANDROID_UIAUTOMATORAndroidPowerful UiSelector queries
iOS PredicateIOS_PREDICATEiOSFast, flexible attribute matching
iOS Class ChainIOS_CLASS_CHAINiOSHierarchy queries, faster than XPath
XPathXPATHBothFlexible but slow and brittle , avoid
from appium.webdriver.common.appiumby import AppiumBy

driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_button")
driver.find_element(AppiumBy.ID, "com.example.app:id/username")
driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
                    'new UiSelector().text("Sign in")')
driver.find_element(AppiumBy.IOS_PREDICATE, "label == 'Sign in'")
driver.find_element(AppiumBy.IOS_CLASS_CHAIN,
                    '**/XCUIElementTypeButton[label == "Sign in"]')

6. Element Interactions

el = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "username")

el.click()
el.send_keys("test_user")
el.clear()
value = el.text
visible = el.is_displayed()
enabled = el.is_enabled()

7. Appium Gestures (mobile: commands, 2.x)

The modern, reliable way to gesture. Coordinates are optional if you pass an elementId. Full parameter references: UiAutomator2 mobile gestures (Android) and XCUITest execute methods (iOS).

# --- Android (UiAutomator2) ---
driver.execute_script("mobile: swipeGesture", {
    "left": 100, "top": 800, "width": 200, "height": 400,
    "direction": "up", "percent": 0.75
})

driver.execute_script("mobile: scrollGesture", {
    "left": 100, "top": 300, "width": 200, "height": 800,
    "direction": "down", "percent": 1.0
})

driver.execute_script("mobile: longClickGesture",
                      {"elementId": el.id, "duration": 1000})

# --- iOS (XCUITest) ---
driver.execute_script("mobile: swipe", {"elementId": el.id, "direction": "up"})
driver.execute_script("mobile: scroll", {"direction": "down"})
driver.execute_script("mobile: pinch", {"elementId": el.id, "scale": 2.0, "velocity": 1.0})

8. Waits — Never Hard-Code Sleeps

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

wait = WebDriverWait(driver, 15)
el = wait.until(
    EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "home_title"))
)
ApproachUse it?Why
Explicit wait (WebDriverWait)YesWaits for a real condition; fast + stable
Implicit waitSparinglyGlobal; can hide problems and slow failures
time.sleep()NoFlaky and slow — a common cause of unstable suites

See the Selenium explicit waits guide for the WebDriverWait / expected_conditions API reused by the Appium Python client.

9. App & Device Management

driver.install_app("/path/to/app.apk")
driver.remove_app("com.example.app")
driver.activate_app("com.example.app")     # bring to foreground
driver.terminate_app("com.example.app")    # kill
driver.background_app(5)                    # send to background for 5s
installed = driver.is_app_installed("com.example.app")

Hybrid apps : switch between native and webview contexts:

print(driver.contexts)                 # ['NATIVE_APP', 'WEBVIEW_com.example.app']
driver.switch_to.context("WEBVIEW_com.example.app")
# ... interact with web content ...
driver.switch_to.context("NATIVE_APP")

10. Debugging & Troubleshooting

Tools: Appium Inspector (inspect the element tree + generate locators), server logs (appium –log-level debug), and driver.save_screenshot(“fail.png”) on failure.

ErrorLikely causeFix
A session is either terminated or not startedServer not running / wrong URL or base pathStart appium; use base path / in 2.x
NoSuchElementExceptionWrong locator or element not yet renderedVerify in Inspector; add an explicit wait
StaleElementReferenceExceptionElement re-rendered after lookupRe-find the element right before use
An unknown server-side error… could not find appBad app path or missing capabilityUse an absolute path/URL; check identity caps
Session created but wrong deviceAmbiguous capsPin udid (and platformVersion)

11. Putting It Together : A Complete Appium Tutorial Example

Everything above in one runnable file: a pytest test that launches an Android app, waits for the login screen, signs in, scrolls to a settings row, and asserts the result. A pytest fixture handles session setup and guaranteed teardown.

# test_login.py
# Run with:  pytest test_login.py
# Requires:  pip install Appium-Python-Client pytest   (Appium 2.x server running)

import os
import pytest
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

APPIUM_SERVER = "http://127.0.0.1:4723"   # 2.x base path is "/"


@pytest.fixture
def driver():
    options = UiAutomator2Options()
    options.platform_name = "Android"
    options.device_name = "Pixel 7"
    options.app = "/path/to/app.apk"
    options.app_package = "com.example.app"
    options.app_activity = ".MainActivity"
    options.no_reset = True
    options.automation_name = "UiAutomator2"

    drv = webdriver.Remote(APPIUM_SERVER, options=options)
    yield drv
    drv.quit()   # always release the session, even on failure


def test_user_can_log_in(driver):
    wait = WebDriverWait(driver, 15)

    # 1. Wait for the login screen, then sign in
    username = wait.until(
        EC.visibility_of_element_located(
            (AppiumBy.ACCESSIBILITY_ID, "username_field"))
    )
    username.send_keys("test_user")
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "password_field").send_keys(os.environ["TEST_PASSWORD"])  # read the secret from the env, never hard-code it
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_button").click()

    # 2. Wait for the home screen to confirm login succeeded
    home = wait.until(
        EC.visibility_of_element_located(
            (AppiumBy.ACCESSIBILITY_ID, "home_title"))
    )
    assert home.is_displayed()

    # 3. Scroll down and open Settings (gesture + interaction)
    driver.execute_script("mobile: scrollGesture", {
        "left": 100, "top": 300, "width": 400, "height": 1200,
        "direction": "down", "percent": 1.0
    })
    settings = wait.until(
        EC.element_to_be_clickable(
            (AppiumBy.ACCESSIBILITY_ID, "settings_row"))
    )
    settings.click()

    # 4. Assert we landed on the Settings screen
    header = wait.until(
        EC.visibility_of_element_located(
            (AppiumBy.ACCESSIBILITY_ID, "settings_header"))
    )
    assert header.text == "Settings"

What this demonstrates: typed Options (§3), a session with the 2.x base path (§4), accessibility-ID locators (§5), interactions (§6), a mobile: gesture (§7), explicit waits instead of sleeps (§8), and clean teardown via the fixture.

iOS variant (XCUITest)

The same test shape on iOS. Only two things change: the Options object (XCUITestOptions + bundle_id) and the gesture command (mobile: scroll instead of mobile: scrollGesture). The locators here reuse accessibility IDs, so they stay identical — one more reason to prefer them (§5).

Because this block sets bundle_id but not app, it assumes the app is already installed on the device. Add options.app = “/path/to/app.ipa” (or call driver.install_app(…)) if you need Appium to install it first.

# test_login_ios.py
# Run with:  pytest test_login_ios.py
# Requires:  pip install Appium-Python-Client pytest   (Appium 2.x server running)

import os
import pytest
from appium import webdriver
from appium.options.ios import XCUITestOptions
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

APPIUM_SERVER = "http://127.0.0.1:4723"   # 2.x base path is "/"


@pytest.fixture
def driver():
    options = XCUITestOptions()
    options.platform_name = "iOS"
    options.device_name = "iPhone 15"
    options.platform_version = "17.2"
    options.bundle_id = "com.example.app"
    options.no_reset = True
    options.automation_name = "XCUITest"

    drv = webdriver.Remote(APPIUM_SERVER, options=options)
    yield drv
    drv.quit()   # always release the session, even on failure


def test_user_can_log_in(driver):
    wait = WebDriverWait(driver, 15)

    # 1. Wait for the login screen, then sign in
    username = wait.until(
        EC.visibility_of_element_located(
            (AppiumBy.ACCESSIBILITY_ID, "username_field"))
    )
    username.send_keys("test_user")
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "password_field").send_keys(os.environ["TEST_PASSWORD"])  # read the secret from the env, never hard-code it
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_button").click()

    # 2. Wait for the home screen to confirm login succeeded
    home = wait.until(
        EC.visibility_of_element_located(
            (AppiumBy.ACCESSIBILITY_ID, "home_title"))
    )
    assert home.is_displayed()

    # 3. Scroll down and open Settings (iOS gesture + interaction)
    driver.execute_script("mobile: scroll", {"direction": "down"})
    settings = wait.until(
        EC.element_to_be_clickable(
            (AppiumBy.ACCESSIBILITY_ID, "settings_row"))
    )
    settings.click()

    # 4. Assert we landed on the Settings screen
    header = wait.until(
        EC.visibility_of_element_located(
            (AppiumBy.ACCESSIBILITY_ID, "settings_header"))
    )
    assert header.text == "Settings"

Tip: In a real suite, keep the test body in one place and parametrize the fixture (e.g. a pytest fixture that yields either UiAutomator2Options or XCUITestOptions) so the same assertions run on both platforms without duplicating the test logic.

12. Mobile Automation Best Practices

  • Use the Page Object pattern. Wrap screens so tests read as intent (login_page.sign_in(user)), not scattered taps.
  • Prefer accessibility IDs. Ask developers to add them — it improves both automation stability and real accessibility.
  • One session, one clean state. Reset app state deliberately; don’t let tests depend on each other’s data.
  • Validate on real devices before release. Emulators and simulators are great for speed, but gestures, sensors, biometrics, and OS-specific behavior only show up on real hardware , see best practices for real-device testing.
  • Parallelize across a device matrix. Mobile UI tests are slow; run them in parallel to keep CI feedback practical.

A minimal Page Object keeps intent and locators in one place:

class LoginPage:
    def __init__(self, driver):
        self.driver = driver

    def sign_in(self, user, password):
        self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "username_field").send_keys(user)
        self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "password_field").send_keys(password)
        self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_button").click()

# in the test:
LoginPage(driver).sign_in("test_user", os.environ["TEST_PASSWORD"])

Scale Beyond Your Local Machine

An Appium tutorial like this one gets your first tests running. Scaling them across the device/OS matrix your users actually run is the harder part , and maintaining a physical device lab (buying, charging, updating, replacing hardware) quickly becomes its own project.

Kobiton provides a real-device cloud so you can run these same Appium scripts across many real Android and iOS devices in parallel, straight from CI ,no hardware to manage. Point your webdriver.Remote at Kobiton’s endpoint, keep your test code, and expand coverage on demand.