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
- Prerequisites & Setup
- Appium Architecture
- Configuring Desired Capabilities
- Page Object Model for Mobile
- Real Device Testing
- Emulator vs Real Device
- CI/CD Integration
- Real Project Example
- Common Issues
- 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 devicesor 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
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.