TL;DR

  • Appium automates iOS and Android apps with WebDriver protocol — one framework, both platforms
  • Setup: Install Appium server, platform SDKs (Android Studio/Xcode), and client library
  • Find elements by accessibility id, xpath, or platform-specific locators
  • Supports gestures (swipe, scroll, tap), real devices, and emulators/simulators
  • Integrates with CI/CD via Appium server in Docker or cloud services (BrowserStack, Sauce Labs)

Best for: QA teams testing mobile apps across platforms, cross-platform automation Skip if: Testing only Android (use Espresso) or only iOS (use XCUITest) Reading time: 20 minutes

Your app works on iOS. Crashes on Android. Or vice versa. Manual testing on both platforms doubles your effort. Feature parity becomes a nightmare.

Appium solves cross-platform mobile testing. Write one test, run on iOS and Android. Same language, same framework, same CI pipeline.

This tutorial covers Appium from setup to CI/CD integration — everything you need to automate mobile apps.

What is Appium?

Appium is an open-source mobile automation framework. It automates native, hybrid, and mobile web apps on iOS and Android using the WebDriver protocol.

How Appium works:

  1. Your test code sends commands to Appium server
  2. Appium translates commands to platform-specific actions
  3. UIAutomator2 (Android) or XCUITest (iOS) executes the actions
  4. Results return through the same chain

Why Appium:

  • Cross-platform — same tests for iOS and Android
  • Language-agnostic — Java, Python, JavaScript, Ruby, C#
  • No app modification — tests real production builds
  • WebDriver standard — familiar API for Selenium users
  • Open-source — free, active community, regular updates

Environment Setup

Prerequisites

For Android:

  • Java JDK 11+
  • Android Studio with SDK
  • ANDROID_HOME environment variable
  • USB debugging enabled on device

For iOS (macOS only):

  • Xcode with Command Line Tools
  • iOS Simulator or real device
  • Apple Developer account (for real devices)

Installing Appium

# Install Appium 2.x globally
npm install -g appium

# Verify installation
appium --version

# Install platform drivers
appium driver install uiautomator2  # Android
appium driver install xcuitest      # iOS

# List installed drivers
appium driver list --installed

Appium Inspector

Install Appium Inspector for element inspection:

Desired Capabilities

Capabilities tell Appium what device and app to use.

Android Capabilities

from appium import webdriver
from appium.options.android import UiAutomator2Options

options = UiAutomator2Options()
options.platform_name = "Android"
options.device_name = "Pixel 6"
options.app = "/path/to/app.apk"
options.automation_name = "UiAutomator2"

# Optional but recommended
options.no_reset = True  # Don't reset app state
options.full_reset = False
options.new_command_timeout = 300

driver = webdriver.Remote("http://localhost:4723", options=options)

iOS Capabilities

from appium import webdriver
from appium.options.ios import XCUITestOptions

options = XCUITestOptions()
options.platform_name = "iOS"
options.device_name = "iPhone 14"
options.platform_version = "16.0"
options.app = "/path/to/app.app"
options.automation_name = "XCUITest"

# For real devices
options.udid = "device-udid-here"

driver = webdriver.Remote("http://localhost:4723", options=options)

Testing Installed Apps

# Android - use app package and activity
options.app_package = "com.example.myapp"
options.app_activity = "com.example.myapp.MainActivity"

# iOS - use bundle ID
options.bundle_id = "com.example.myapp"

Finding Elements

Locator Strategies

from appium.webdriver.common.appiumby import AppiumBy

# Accessibility ID (recommended - works cross-platform)
element = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_button")

# ID (Android resource-id)
element = driver.find_element(AppiumBy.ID, "com.example:id/login_button")

# XPath (slower, but flexible)
element = driver.find_element(AppiumBy.XPATH, "//android.widget.Button[@text='Login']")

# Class name
element = driver.find_element(AppiumBy.CLASS_NAME, "android.widget.EditText")

# Android UIAutomator
element = driver.find_element(
    AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiSelector().text("Login")'
)

# iOS predicate string
element = driver.find_element(
    AppiumBy.IOS_PREDICATE,
    'label == "Login" AND type == "XCUIElementTypeButton"'
)

# iOS class chain
element = driver.find_element(
    AppiumBy.IOS_CLASS_CHAIN,
    '**/XCUIElementTypeButton[`label == "Login"`]'
)

Waiting for Elements

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)

# Wait for element to be visible
element = wait.until(
    EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "welcome_message"))
)

# Wait for element to be clickable
element = wait.until(
    EC.element_to_be_clickable((AppiumBy.ACCESSIBILITY_ID, "submit_button"))
)

# Wait for element to disappear
wait.until(
    EC.invisibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "loading_spinner"))
)

Basic Interactions

Tap, Type, Clear

# Tap/Click
login_button = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_button")
login_button.click()

# Type text
username_field = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "username")
username_field.send_keys("testuser")

# Clear field
username_field.clear()

# Get text
message = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "message")
print(message.text)

# Check if displayed/enabled
assert login_button.is_displayed()
assert login_button.is_enabled()

Handling Keyboard

# Hide keyboard
driver.hide_keyboard()

# Check if keyboard shown (Android)
is_keyboard_shown = driver.is_keyboard_shown()

Gestures

Swipe and Scroll

from appium.webdriver.common.touch_action import TouchAction

# Simple swipe (start_x, start_y, end_x, end_y, duration_ms)
driver.swipe(500, 1500, 500, 500, 800)

# Scroll to element (Android)
driver.find_element(
    AppiumBy.ANDROID_UIAUTOMATOR,
    'new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Target Text"))'
)

# Scroll direction
def scroll_down(driver):
    size = driver.get_window_size()
    start_x = size['width'] // 2
    start_y = size['height'] * 0.8
    end_y = size['height'] * 0.2
    driver.swipe(start_x, start_y, start_x, end_y, 500)

def scroll_up(driver):
    size = driver.get_window_size()
    start_x = size['width'] // 2
    start_y = size['height'] * 0.2
    end_y = size['height'] * 0.8
    driver.swipe(start_x, start_y, start_x, end_y, 500)
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.actions import interaction
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.pointer_input import PointerInput

# Tap at coordinates
actions = ActionChains(driver)
actions.w3c_actions = ActionBuilder(driver, mouse=PointerInput(interaction.POINTER_TOUCH, "touch"))
actions.w3c_actions.pointer_action.move_to_location(100, 200)
actions.w3c_actions.pointer_action.pointer_down()
actions.w3c_actions.pointer_action.pause(0.1)
actions.w3c_actions.pointer_action.release()
actions.perform()

# Long press
element = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "item")
actions = ActionChains(driver)
actions.click_and_hold(element).pause(2).release().perform()

Complete Test Example

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


class TestLoginFlow:
    @pytest.fixture(autouse=True)
    def setup(self):
        options = UiAutomator2Options()
        options.platform_name = "Android"
        options.device_name = "emulator-5554"
        options.app = "./app-debug.apk"
        options.automation_name = "UiAutomator2"
        options.no_reset = False

        self.driver = webdriver.Remote("http://localhost:4723", options=options)
        self.wait = WebDriverWait(self.driver, 15)
        yield
        self.driver.quit()

    def test_successful_login(self):
        # Enter username
        username = self.wait.until(
            EC.presence_of_element_located((AppiumBy.ACCESSIBILITY_ID, "username_input"))
        )
        username.send_keys("testuser@example.com")

        # Enter password
        password = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "password_input")
        password.send_keys("securepassword123")

        # Hide keyboard
        self.driver.hide_keyboard()

        # Tap login button
        login_btn = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_button")
        login_btn.click()

        # Verify welcome message
        welcome = self.wait.until(
            EC.presence_of_element_located((AppiumBy.ACCESSIBILITY_ID, "welcome_message"))
        )
        assert "Welcome" in welcome.text

    def test_invalid_credentials(self):
        username = self.wait.until(
            EC.presence_of_element_located((AppiumBy.ACCESSIBILITY_ID, "username_input"))
        )
        username.send_keys("wrong@example.com")

        password = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "password_input")
        password.send_keys("wrongpassword")

        self.driver.hide_keyboard()

        login_btn = self.driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login_button")
        login_btn.click()

        # Verify error message
        error = self.wait.until(
            EC.presence_of_element_located((AppiumBy.ACCESSIBILITY_ID, "error_message"))
        )
        assert "Invalid credentials" in error.text

Page Object Pattern

# pages/base_page.py
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 15)

    def find(self, locator):
        return self.wait.until(EC.presence_of_element_located(locator))

    def click(self, locator):
        self.find(locator).click()

    def type_text(self, locator, text):
        element = self.find(locator)
        element.clear()
        element.send_keys(text)


# pages/login_page.py
class LoginPage(BasePage):
    USERNAME = (AppiumBy.ACCESSIBILITY_ID, "username_input")
    PASSWORD = (AppiumBy.ACCESSIBILITY_ID, "password_input")
    LOGIN_BTN = (AppiumBy.ACCESSIBILITY_ID, "login_button")
    ERROR_MSG = (AppiumBy.ACCESSIBILITY_ID, "error_message")

    def login(self, username, password):
        self.type_text(self.USERNAME, username)
        self.type_text(self.PASSWORD, password)
        self.driver.hide_keyboard()
        self.click(self.LOGIN_BTN)

    def get_error_message(self):
        return self.find(self.ERROR_MSG).text


# tests/test_login.py
class TestLogin:
    def test_login_success(self, driver):
        login_page = LoginPage(driver)
        login_page.login("user@example.com", "password123")
        # assertions...

Real Device Testing

Android Real Device

# Connect device via USB
adb devices

# Get device name
adb shell getprop ro.product.model

# Enable USB debugging in Developer Options
options.device_name = "Pixel 6"
options.udid = "emulator-5554"  # or actual device serial

iOS Real Device

Requires provisioning profile and signing:

options.udid = "00008030-001234567890"
options.xcode_org_id = "TEAM_ID"
options.xcode_signing_id = "iPhone Developer"

Cloud Device Farms

BrowserStack:

from appium import webdriver

options = {
    "platformName": "Android",
    "appium:deviceName": "Samsung Galaxy S23",
    "appium:platformVersion": "13.0",
    "appium:app": "bs://app-id",
    "bstack:options": {
        "userName": "YOUR_USERNAME",
        "accessKey": "YOUR_ACCESS_KEY",
        "projectName": "My Project",
        "buildName": "Build 1.0"
    }
}

driver = webdriver.Remote(
    "https://hub-cloud.browserstack.com/wd/hub",
    options=options
)

CI/CD Integration

GitHub Actions

name: Mobile Tests

on: [push, pull_request]

jobs:
  android-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          pip install appium-python-client pytest
          npm install -g appium
          appium driver install uiautomator2

      - name: Start Android emulator
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          script: |
            appium &
            sleep 10
            pytest tests/android/ --junitxml=results.xml

      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: results.xml

Docker Setup

FROM appium/appium

# Install Python and dependencies
RUN apt-get update && apt-get install -y python3 python3-pip
RUN pip3 install appium-python-client pytest

COPY tests/ /tests/
WORKDIR /tests

CMD ["pytest", "-v"]

Appium with AI Assistance

AI tools can help write and debug Appium tests.

What AI does well:

  • Generate page objects from app screenshots
  • Suggest locator strategies
  • Debug element not found errors
  • Convert tests between languages

What still needs humans:

  • Understanding app business logic
  • Choosing between flaky and stable locators
  • Optimizing test execution time
  • Handling platform-specific edge cases

FAQ

What is Appium?

Appium is an open-source mobile automation framework for testing iOS and Android applications. It uses the WebDriver protocol, allowing you to write tests in any language that has a WebDriver client (Java, Python, JavaScript, Ruby, C#). Appium can test native apps, hybrid apps, and mobile web apps without requiring app modification — you test real production builds.

Is Appium free?

Yes, Appium is completely free and open-source under the Apache 2.0 license. There are no enterprise editions, premium features, or usage limits. The entire framework, including all drivers (UiAutomator2, XCUITest), is available at no cost.

Appium vs Espresso/XCUITest — which is better?

Appium excels at cross-platform testing — write tests once, run on both iOS and Android. Espresso (Android) and XCUITest (iOS) are platform-specific but faster, more stable, and have deeper OS integration. Use Appium when you need cross-platform coverage with a single codebase. Use native tools when you need maximum speed and reliability for one platform.

Can Appium test both iOS and Android?

Yes, this is Appium’s primary advantage. The same test code can run on both platforms with minimal changes — usually just different locators for platform-specific elements. You write tests in your preferred language, and Appium translates commands to the appropriate platform automation (UiAutomator2 for Android, XCUITest for iOS).

Official Resources

See Also