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 deviceAppium 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-Client3. 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.
| Capability | Android | iOS | Purpose |
|---|---|---|---|
| platformName | Android | iOS | Target platform |
| automationName | UiAutomator2 | XCUITest | Driver to use |
| deviceName | Pixel 7 | iPhone 15 | Device label |
| platformVersion | 14 | 17.2 | OS version |
| app | path/URL to .apk | path/URL to .ipa/.app | Build under test |
| App identity | appPackage + appActivity | bundleId | Launch target |
| udid | real device serial | real device UDID | Specific device |
| noReset / fullReset | ✔ | ✔ | Control 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 sessionRunning 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.
| Strategy | AppiumBy constant | Platform | Notes |
|---|---|---|---|
| Accessibility ID | ACCESSIBILITY_ID | Both | Generally the most reliable — stable, cross-platform |
| ID / resource-id | ID | Android | Fast and stable when devs add IDs |
| Class name | CLASS_NAME | Both | Coarse; returns many matches |
| UiAutomator | ANDROID_UIAUTOMATOR | Android | Powerful UiSelector queries |
| iOS Predicate | IOS_PREDICATE | iOS | Fast, flexible attribute matching |
| iOS Class Chain | IOS_CLASS_CHAIN | iOS | Hierarchy queries, faster than XPath |
| XPath | XPATH | Both | Flexible 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"))
)| Approach | Use it? | Why |
|---|---|---|
| Explicit wait (WebDriverWait) | Yes | Waits for a real condition; fast + stable |
| Implicit wait | Sparingly | Global; can hide problems and slow failures |
| time.sleep() | No | Flaky 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.
| Error | Likely cause | Fix |
|---|---|---|
| A session is either terminated or not started | Server not running / wrong URL or base path | Start appium; use base path / in 2.x |
| NoSuchElementException | Wrong locator or element not yet rendered | Verify in Inspector; add an explicit wait |
| StaleElementReferenceException | Element re-rendered after lookup | Re-find the element right before use |
| An unknown server-side error… could not find app | Bad app path or missing capability | Use an absolute path/URL; check identity caps |
| Session created but wrong device | Ambiguous caps | Pin 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.
