Skip to main content
/tayyab/portfolio — zsh
tayyab
TA
// dispatch.read --classified=false --access-level: public

Appium Mobile Testing Tutorial 2026: iOS & Android Automation from Scratch

February 27, 2026 EST. READ: 15 MIN #Test Automation

Appium Mobile Testing Tutorial 2026: iOS & Android Automation from Scratch

Mobile automation is different. It's not just a smaller version of web testing.

I've automated tests for 3 mobile apps (iOS + Android). The first project was a disaster — I treated mobile testing like web testing and learned the hard way.

Appium is the industry standard for cross-platform mobile testing. This guide covers what I wish I knew starting out: setup, Page Object Model, real device testing, and CI/CD integration.

Table of Contents

  1. Prerequisites & Setup
  2. Appium Architecture
  3. Configuring Desired Capabilities
  4. Page Object Model for Mobile
  5. Real Device Testing
  6. Emulator vs Real Device
  7. CI/CD Integration
  8. Real Project Example
  9. Common Issues
  10. FAQ

Prerequisites & Setup

What You Need

macOS (for iOS testing):

  • Xcode 14+ (includes iOS simulator)
  • Xcode Command Line Tools
  • Node.js 16+
  • Appium Server
  • WebDriverIO or Playwright (for test writing)

Any OS (for Android):

  • Android SDK (API level 30+)
  • Android emulator or real device
  • Java 11+ (for APK building)
  • Node.js 16+
  • Appium Server

Install Appium

# Install Appium globally
npm install -g appium

# Verify installation
appium --version
# Output: 2.0.0

# Install drivers for iOS and Android
appium driver install uiautomator2  # Android driver
appium driver install xcui          # iOS driver

# Verify drivers
appium driver list

Project Setup

# Create test project
mkdir mobile-automation && cd mobile-automation
npm init -y

# Install WebDriverIO (recommended for mobile)
npm install --save-dev @wdio/cli @wdio/mocha-framework @wdio/appium-service

# Create WDIO config
npx wdio config
# Follow prompts: Select mocha, appium service, WebDriver protocol

Appium Architecture

How Appium Works

Appium is a wrapper around native automation frameworks:

Your Test Code (JavaScript/Python)
        ↓
   Appium Server
        ↓
  XCUITest (iOS)  OR  UiAutomator2 (Android)
        ↓
  Actual Device/Emulator

Key difference from web testing:

  • Web testing: You control a browser
  • Mobile testing: You control the entire app and OS (touch, swipe, orientation)

Configuring Desired Capabilities

Android Real Device Example

// wdio.conf.js
export const config = {
  runner: 'local',
  port: 4723,
  specs: ['./test/specs/**/*.js'],
  
  capabilities: [
    {
      platformName: 'Android',
      'appium:automationName': 'UiAutomator2',
      'appium:deviceName': 'emulator-5554',  // Real device: actual phone name
      'appium:app': '/path/to/app.apk',      // APK path
      'appium:appPackage': 'com.example.app',
      'appium:appActivity': 'MainActivity',
    }
  ],
  
  services: [
    [
      'appium',
      {
        command: 'appium',
        args: ['--relaxed-security'],
      }
    ]
  ]
};

iOS Simulator Example

capabilities: [
  {
    platformName: 'iOS',
    'appium:automationName': 'XCUITest',
    'appium:deviceName': 'iPhone 15',
    'appium:platformVersion': '17.2',
    'appium:app': '/path/to/app.app',  // Built app path
  }
]

Real Device Requirements

Android:

  • Enable Developer Mode: Settings → About → Build Number (tap 7x)
  • Enable USB Debugging: Settings → Developer Options → USB Debugging
  • Connect via USB or wireless ADB
  • Get device name: adb devices

iOS:

  • Connect device via USB
  • Trust the Mac in device Security dialog
  • Get UDID: xcrun xctrace list devices or Xcode Devices window
  • Deploy provisioning profile matching your team ID

Page Object Model for Mobile

BasePage Pattern

// pages/BasePage.js
export class BasePage {
  constructor(driver) {
    this.driver = driver;
  }

  async click(elementId) {
    await this.driver.$(elementId).click();
  }

  async type(elementId, text) {
    const element = await this.driver.$(elementId);
    await element.clearValue();
    await element.setValue(text);
  }

  async getText(elementId) {
    return await this.driver.$(elementId).getText();
  }

  async isDisplayed(elementId) {
    return await this.driver.$(elementId).isDisplayed();
  }

  // Mobile-specific: Swipe
  async swipeUp(duration = 500) {
    await this.driver.touchAction([
      { action: 'press', x: 250, y: 600 },
      { action: 'moveTo', x: 250, y: 200 },
      { action: 'release' }
    ]);
  }

  // Mobile-specific: Long press
  async longPress(elementId, duration = 2000) {
    const element = await this.driver.$(elementId);
    await element.touchAction([{ action: 'longPress', duration }]);
  }
}

LoginPage Implementation

// pages/LoginPage.js
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  // Locators (native format for mobile)
  get emailField() {
    return '~emailInput';  // Accessibility ID
  }

  get passwordField() {
    return '~passwordInput';
  }

  get loginButton() {
    return '~loginButton';
  }

  // Alternative: XPath
  get forgotPasswordLink() {
    return '//XCUIElementTypeButton[@name="Forgot Password"]';
  }

  async login(email, password) {
    await this.type(this.emailField, email);
    await this.type(this.passwordField, password);
    await this.click(this.loginButton);
  }
}

Test Example

// test/specs/login.spec.js
import { LoginPage } from '../pages/LoginPage';

describe('Mobile Login Tests', () => {
  let loginPage;

  beforeEach(() => {
    loginPage = new LoginPage(driver);
  });

  it('User can login with valid credentials', async () => {
    await loginPage.login('user@example.com', 'password123');
    
    const dashboardText = await driver.$('~dashboardTitle').getText();
    expect(dashboardText).toBe('Welcome');
  });

  it('User sees error with invalid password', async () => {
    await loginPage.login('user@example.com', 'wrongpassword');
    
    const errorMessage = await driver.$('~errorMessage').getText();
    expect(errorMessage).toContain('Invalid credentials');
  });
});

Real Device Testing

Connect Physical Device (Android)

# Check connected devices
adb devices
# Output:
# List of attached devices
# emulator-5554          device
# FA8H91H12E             device

# Connect wireless (same network)
adb connect 192.168.1.100:5037

# Grant permissions to test app
adb shell pm grant com.example.app android.permission.CAMERA
adb shell pm grant com.example.app android.permission.WRITE_EXTERNAL_STORAGE

Install App on Device

# Android APK
adb install -r app-release.apk

# iOS (requires provisioning)
xcrun simctl install booted path/to/app.app

Run Tests on Real Device

// wdio.conf.js - Real device capabilities
capabilities: [{
  platformName: 'Android',
  'appium:automationName': 'UiAutomator2',
  'appium:deviceName': 'FA8H91H12E',  // Real device UDID
  'appium:app': '/path/to/app.apk',
}]

Emulator vs Real Device

Emulator Advantages

  • Free and instant (no hardware needed)
  • Different OS versions easily
  • Reproducible (same state every time)

Emulator Disadvantages

  • Slow (needs 2-3x longer for tests)
  • Missing features (some sensors, cameras)
  • Flaky (performance varies by machine)

Real Device Advantages

  • Tests actual hardware behavior
  • Real performance metrics
  • Catches device-specific bugs

Real Device Disadvantages

  • Expensive (need multiple devices)
  • Setup complexity
  • Device farms required for CI/CD

Recommendation: Emulator for development, real devices for final validation.


CI/CD Integration

GitHub Actions with Android Emulator

name: Mobile Automation Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: macos-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Install Node
        uses: actions/setup-node@v4
        with:
          node-version: 18
      
      - name: Install Appium
        run: npm install -g appium @appium/driver-uiautomator2
      
      - name: Start Appium Server
        run: |
          appium &
          sleep 5
      
      - name: Build APK
        run: |
          # Build your app
          # ./gradlew assembleDebug
      
      - name: Install dependencies
        run: npm install
      
      - name: Run tests
        run: npm run test:mobile
        env:
          APK_PATH: ./app-debug.apk
      
      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: mobile-test-results
          path: ./test-results/

Real Project Example

On a fintech mobile app (Android + iOS):

Setup:

  • 120 Appium tests (85% Android-focused, 15% iOS-specific)
  • Page Objects: 18 screens
  • Real devices: 3 Android phones, 2 iPhones
  • Emulator tests: Pre-commit validation

Results:

  • Emulator: 8 minutes (development cycle)
  • Real devices: 25 minutes (gate before release)
  • Bug detection: 82% of UI crashes caught before production
  • Device coverage: 8 unique Android versions, 3 iOS versions

Challenges:

  • Flaky tests: 15% → 2% (proper waits, no hardcoded sleeps)
  • Device sync: Coordinating test execution across real devices (used device farms)
  • Element detection: Accessibility IDs missing → worked with dev team to add them

Common Issues

Issue: "Element not found"

Cause: Locator strategy wrong for mobile
Fix: Use Appium Inspector to find correct locator

appium inspector
# Launches UI inspector on localhost:4723

Issue: "Appium connection refused"

Cause: Appium server not running
Fix:

# Start Appium
appium

# OR check if port 4723 is in use
lsof -i :4723

Issue: "Device offline"

Cause: USB connection lost or ADB daemon issue
Fix:

adb reconnect device
adb kill-server
adb start-server

Issue: Tests work on emulator but fail on real device

Cause: Missing permissions, different app signing
Fix: Check device permissions, ensure app signed with correct certificate


FAQ

Q: Appium or Espresso/XCTest?
A: Appium for cross-platform. Espresso (Android) and XCTest (iOS) are faster but require language-specific setup and don't share test code.

Q: How many devices do I need to test?
A: Minimum: 2 Android, 2 iOS (different OS versions). Use device clouds (BrowserStack, Sauce Labs) for more.

Q: Can I test iOS on Windows/Linux?
A: No. iOS testing requires macOS. Android works on any platform.

Q: How to handle app permissions?
A: Grant via Desired Capabilities or adb shell commands before test run.

Q: Performance impact of real device testing?
A: 2-3x slower than emulator, but mandatory for release gates.

Tayyab Akmal
// author

Tayyab Akmal

AI & QA Automation Engineer

I've caught critical bugs in fintech, e-commerce, and SaaS platforms — then built the automation that prevents them from shipping again. 6+ years scaling test automation and AI-driven QA.

// feedback_channel

FOUND THIS USEFUL?

Share your thoughts or let's discuss automation testing strategies.

→ Start Conversation
Available for hire