Appium Mobile Testing Guide

← Docs v2.0+
1

Environment Setup

Setup

Appium is an open-source test automation framework for mobile applications supporting native, hybrid, and mobile web apps on iOS and Android.

Prerequisites

  1. Node.js (v18 or higher)
  2. Android Studio (for Android testing)
  3. Xcode (for iOS testing - macOS only)
  4. Java JDK (Adoptium OpenJDK 11 or 17)

Android Setup

bash
# Install WebDriverIO with Appium
npm init wdio .

# Install Appium
npm install --save-dev appium

# Install Android driver
npx appium driver install uiautomator2

# Verify installation
npx appium driver list --installed

# Start Appium server
npx appium -p 4724 --allow-cors

Environment Variables (Windows)

text
# Android SDK
ANDROID_HOME=C:\Users\YourUsername\AppData\Local\Android\Sdk
ANDROID_SDK_ROOT=C:\Users\YourUsername\AppData\Local\Android\Sdk

# Java JDK
JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-17.0.x

# Add to PATH:
%ANDROID_HOME%\platform-tools
%ANDROID_HOME%\tools
%JAVA_HOME%\bin
💡 Tip: Use npx appium doctor to verify your setup.
2

WebDriverIO Config

Config

WebDriverIO is configured via wdio.conf.js. You can have separate configs for Android and iOS.

wdio.android.conf.js

javascript
exports.config = {
  port: 4724,
  specs: ['./test/specs/android/**/*.js'],
  maxInstances: 1,
  capabilities: [{
    platformName: 'Android',
    'appium:platformVersion': '13.0',
    'appium:deviceName': 'Pixel 6',
    'appium:automationName': 'UiAutomator2',
    'appium:app': './app/android/app.apk',
    'appium:autoGrantPermissions': true
  }],
  logLevel: 'info',
  framework: 'mocha',
  reporters: ['spec'],
  mochaOpts: { timeout: 60000 }
};

Running Tests

bash
# Run Android tests
npx wdio config/wdio.android.conf.js

# Run iOS tests
npx wdio config/wdio.ios.conf.js

# Check devices
adb devices              # Android
xcrun simctl list       # iOS
3

Locator Strategies

Core

Appium provides multiple ways to locate mobile elements. Prefer accessibility IDs for stability.

Locator Priority

PriorityStrategyExampleUse Case
1 (Best)Accessibility ID~"loginButton"Most stable, cross-platform
2Resource IDid=com.app:id/usernameAndroid-specific, reliable
3Class Chain (iOS)-ios class chain:**/XCUIElementTypeButtoniOS-specific, performant
4XPath//android.widget.Button[@text="Login"]Last resort, slower

Android Locators

javascript
// ✅ GOOD - Accessibility ID
$('~loginButton')

// ✅ GOOD - Resource ID
$('android=new UiSelector().resourceId("com.app:id/loginButton")')

// ✅ GOOD - UiAutomator with text
$('android=new UiSelector().text("Login").className("android.widget.Button")')

// ⚠️ ACCEPTABLE - Simple XPath
$('//android.widget.Button[@resource-id="com.app:id/loginButton"]')

// ❌ BAD - Absolute XPath
$('/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout[1]/android.widget.Button[2]')

iOS Locators

javascript
// ✅ GOOD - Accessibility ID
$('~loginButton')

// ✅ GOOD - Predicate String
$('-ios predicate string:name == "Login" AND type == "XCUIElementTypeButton"')

// ✅ GOOD - Class Chain
$('-ios class chain:**/XCUIElementTypeButton[`name == "Login"`]')

// ❌ BAD - Complex XPath
$('//XCUIElementTypeApplication/XCUIElementTypeWindow[1]/XCUIElementTypeButton[2]')
4

Actions & Interactions

Actions

All Appium actions are async. Always await them.

javascript
// Click
await element.click();

// Enter text
await element.setValue('text');
await element.addValue('more text');

// Clear text
await element.clearValue();

// Get text
const text = await element.getText();

// Check if displayed
const isDisplayed = await element.isDisplayed();

// Wait for element
await element.waitForDisplayed({ timeout: 10000 });

// Wait for element to disappear
await element.waitForDisplayed({ reverse: true, timeout: 5000 });

// Swipe
await driver.execute('mobile: swipe', { direction: 'up' });

// Hide keyboard
await driver.hideKeyboard();
5

Assertions

Assertions

WebDriverIO uses expect assertions that auto-retry until timeout.

javascript
// Visibility
await expect(element).toBeDisplayed();
await expect(element).not.toBeDisplayed();

// Text content
await expect(element).toHaveText('Expected Text');
await expect(element).toHaveTextContaining('Partial');

// Attributes
await expect(element).toHaveAttr('enabled', 'true');

// Existence
await expect(element).toExist();
await expect(element).not.toExist();

// Enabled/Disabled
await expect(element).toBeEnabled();
await expect(element).toBeDisabled();
6

Page Object Model

Architecture

Always use Page Object Model (POM) for maintainable mobile test automation.

Screen Object Structure

javascript
// test/screenobjects/android/login.screen.js
class LoginScreen {
  // Element getters
  get usernameField() { return $('~username-input'); }
  get passwordField() { return $('~password-input'); }
  get loginButton() { return $('~login-button'); }
  get errorMessage() { return $('~error-message'); }
  
  // Action methods
  async login(username, password) {
    await this.usernameField.setValue(username);
    await this.passwordField.setValue(password);
    await this.loginButton.click();
  }
  
  async getErrorText() {
    return await this.errorMessage.getText();
  }
}

export default new LoginScreen();

Base Screen Class

javascript
// test/screenobjects/BaseScreen.js
class BaseScreen {
  async waitForElement(element, timeout = 10000) {
    await element.waitForDisplayed({ timeout });
  }
  
  async tapElement(element) {
    await this.waitForElement(element);
    await element.click();
  }
  
  async enterText(element, text) {
    await this.waitForElement(element);
    await element.setValue(text);
  }
  
  async hideKeyboard() {
    if (await driver.isKeyboardShown()) {
      await driver.hideKeyboard();
    }
  }
}

export default BaseScreen;
7

Test Structure

Architecture

Follow the Arrange-Act-Assert pattern for clear, maintainable tests.

javascript
// test/specs/android/login.spec.js
import LoginScreen from '../../screenobjects/android/login.screen';
import HomeScreen from '../../screenobjects/android/home.screen';

describe('Login Functionality', () => {
  beforeEach(async () => {
    await driver.reset();
  });
  
  it('should login successfully with valid credentials', async () => {
    // Arrange
    const username = 'testuser@example.com';
    const password = 'Test@1234';
    
    // Act
    await LoginScreen.login(username, password);
    
    // Assert
    await expect(HomeScreen.welcomeMessage).toBeDisplayed();
  });
  
  it('should show error with invalid credentials', async () => {
    // Arrange
    const username = 'invalid@example.com';
    const password = 'wrongpass';
    
    // Act
    await LoginScreen.login(username, password);
    
    // Assert
    await expect(LoginScreen.errorMessage).toBeDisplayed();
    await expect(LoginScreen.errorMessage).toHaveText('Invalid credentials');
  });
});
💡 Naming: Use "should [expected behavior] when [condition]" format for test names.
8

Wait Strategies

Best Practices

Never use hard-coded sleeps! Always use explicit waits.

javascript
// ❌ BAD - Hard-coded sleep
await driver.pause(5000);

// ✅ GOOD - Wait for element
await element.waitForDisplayed({ timeout: 10000 });

// ✅ GOOD - Wait for clickable
await element.waitForClickable({ timeout: 5000 });

// ✅ GOOD - Wait for element to disappear
await loadingSpinner.waitForDisplayed({ reverse: true, timeout: 15000 });

// ✅ GOOD - Custom wait condition
await driver.waitUntil(
  async () => (await element.getText()) === 'Expected Text',
  { timeout: 10000, timeoutMsg: 'Text did not match' }
);
9

Mobile Gestures

Best Practices

Appium provides mobile-specific gestures for realistic user interactions.

javascript
// Scroll to element (Android)
await driver.execute('mobile: scroll', {
  strategy: 'accessibility id',
  selector: 'targetElement'
});

// Swipe gesture
await driver.execute('mobile: swipe', {
  direction: 'up',
  percent: 0.75
});

// Long press
await element.touchAction([
  { action: 'press', x: 100, y: 200 },
  { action: 'wait', ms: 1000 },
  { action: 'release' }
]);

// Drag and drop
await driver.touchAction([
  { action: 'press', element: sourceElement },
  { action: 'wait', ms: 500 },
  { action: 'moveTo', element: targetElement },
  { action: 'release' }
]);
10

App State Management

Best Practices

Manage app lifecycle for reliable test execution.

javascript
// Reset app to initial state
await driver.reset();

// Terminate and relaunch
await driver.terminateApp('com.example.app');
await driver.activateApp('com.example.app');

// Background app for 5 seconds
await driver.background(5);

// Install app
await driver.installApp('/path/to/app.apk');

// Remove app
await driver.removeApp('com.example.app');

// Check if installed
const isInstalled = await driver.isAppInstalled('com.example.app');
11

WebView Context

Advanced

Switch between native and web contexts for hybrid apps.

javascript
// Get all contexts
const contexts = await driver.getContexts();
console.log(contexts); // ['NATIVE_APP', 'WEBVIEW_com.example.app']

// Switch to WebView
await driver.switchContext('WEBVIEW_com.example.app');

// Interact with web elements
await $('input[name="username"]').setValue('testuser');

// Switch back to native
await driver.switchContext('NATIVE_APP');
12

Hooks & Lifecycle

Advanced

Hooks execute code at specific points in the test lifecycle.

javascript
// config/wdio.shared.conf.js
exports.config = {
  before: async function (capabilities, specs) {
    // Runs before each test session
    await driver.setTimeout({ implicit: 10000 });
  },
  
  beforeTest: async function (test, context) {
    // Runs before each test
    console.log(`Starting test: ${test.title}`);
    await driver.reset();
  },
  
  afterTest: async function (test, context, { error, passed }) {
    if (!passed) {
      // Take screenshot on failure
      const timestamp = new Date().getTime();
      await driver.saveScreenshot(`./screenshots/FAILED_${test.title}_${timestamp}.png`);
    }
  },
  
  onComplete: function (exitCode, config, capabilities, results) {
    console.log('Test suite completed');
  }
};
13

CI/CD Integration

Tooling

Integrate Appium tests into your CI/CD pipeline.

GitHub Actions Example

yaml
name: Mobile Tests

on: [push, pull_request]

jobs:
  android-tests:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - uses: actions/setup-java@v3
        with:
          distribution: 'adopt'
          java-version: '11'
      - run: npm ci
      - run: npm install -g appium
      - run: appium driver install uiautomator2
      - run: appium &
      - run: npm run test:android
      - uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: screenshots
          path: ./screenshots/
14

BrowserStack Integration

Cloud Testing

BrowserStack provides cloud-based real device testing for mobile applications. Run tests on 3000+ real devices without maintaining physical device labs.

Setup & Authentication

bash
# Install BrowserStack service
npm install --save-dev @wdio/browserstack-service

# Set environment variables
export BROWSERSTACK_USERNAME="your_username"
export BROWSERSTACK_ACCESS_KEY="your_access_key"

BrowserStack Configuration

javascript
// config/wdio.browserstack.conf.js
exports.config = {
  user: process.env.BROWSERSTACK_USERNAME,
  key: process.env.BROWSERSTACK_ACCESS_KEY,
  hostname: 'hub.browserstack.com',
  
  specs: ['./test/specs/**/*.js'],
  maxInstances: 5,
  
  capabilities: [{
    // Android Configuration
    'bstack:options': {
      deviceName: 'Samsung Galaxy S23',
      osVersion: '13.0',
      projectName: 'My Mobile App',
      buildName: 'Android Build v1.0',
      sessionName: 'Login Tests',
      debug: true,
      networkLogs: true,
      appiumLogs: true,
      video: true,
      local: false
    },
    platformName: 'Android',
    'appium:automationName': 'UiAutomator2',
    'appium:app': 'bs://your-app-id-here',
    'appium:autoGrantPermissions': true
  }],
  
  services: [
    ['browserstack', {
      browserstackLocal: false,
      opts: {
        forceLocal: false
      }
    }]
  ],
  
  logLevel: 'info',
  framework: 'mocha',
  reporters: ['spec'],
  mochaOpts: {
    timeout: 60000
  }
};

Upload App to BrowserStack

bash
# Upload Android APK
curl -u "USERNAME:ACCESS_KEY" \
  -X POST "https://api-cloud.browserstack.com/app-automate/upload" \
  -F "file=@/path/to/app.apk"

# Upload iOS IPA
curl -u "USERNAME:ACCESS_KEY" \
  -X POST "https://api-cloud.browserstack.com/app-automate/upload" \
  -F "file=@/path/to/app.ipa"

# Response includes app_url: "bs://abc123def456"

Multi-Device Parallel Testing

javascript
// config/wdio.browserstack.parallel.conf.js
exports.config = {
  user: process.env.BROWSERSTACK_USERNAME,
  key: process.env.BROWSERSTACK_ACCESS_KEY,
  hostname: 'hub.browserstack.com',
  
  maxInstances: 10,
  
  capabilities: [
    // Android Devices
    {
      'bstack:options': {
        deviceName: 'Samsung Galaxy S23',
        osVersion: '13.0',
        projectName: 'Cross-Device Testing',
        buildName: `Build ${new Date().toISOString()}`,
        sessionName: 'Samsung S23 Tests'
      },
      platformName: 'Android',
      'appium:automationName': 'UiAutomator2',
      'appium:app': process.env.BROWSERSTACK_APP_ID
    },
    {
      'bstack:options': {
        deviceName: 'Google Pixel 7',
        osVersion: '13.0',
        projectName: 'Cross-Device Testing',
        buildName: `Build ${new Date().toISOString()}`,
        sessionName: 'Pixel 7 Tests'
      },
      platformName: 'Android',
      'appium:automationName': 'UiAutomator2',
      'appium:app': process.env.BROWSERSTACK_APP_ID
    },
    // iOS Devices
    {
      'bstack:options': {
        deviceName: 'iPhone 15 Pro',
        osVersion: '17',
        projectName: 'Cross-Device Testing',
        buildName: `Build ${new Date().toISOString()}`,
        sessionName: 'iPhone 15 Pro Tests'
      },
      platformName: 'iOS',
      'appium:automationName': 'XCUITest',
      'appium:app': process.env.BROWSERSTACK_APP_ID_IOS
    },
    {
      'bstack:options': {
        deviceName: 'iPhone 14',
        osVersion: '16',
        projectName: 'Cross-Device Testing',
        buildName: `Build ${new Date().toISOString()}`,
        sessionName: 'iPhone 14 Tests'
      },
      platformName: 'iOS',
      'appium:automationName': 'XCUITest',
      'appium:app': process.env.BROWSERSTACK_APP_ID_IOS
    }
  ],
  
  services: ['browserstack']
};

BrowserStack Local Testing

Test apps that access local/internal servers using BrowserStack Local.

javascript
// config/wdio.browserstack.local.conf.js
exports.config = {
  user: process.env.BROWSERSTACK_USERNAME,
  key: process.env.BROWSERSTACK_ACCESS_KEY,
  hostname: 'hub.browserstack.com',
  
  capabilities: [{
    'bstack:options': {
      deviceName: 'Samsung Galaxy S23',
      osVersion: '13.0',
      local: true,  // Enable BrowserStack Local
      localIdentifier: 'my-local-connection'
    },
    platformName: 'Android',
    'appium:automationName': 'UiAutomator2',
    'appium:app': process.env.BROWSERSTACK_APP_ID
  }],
  
  services: [
    ['browserstack', {
      browserstackLocal: true,  // Start Local binary
      opts: {
        localIdentifier: 'my-local-connection',
        forceLocal: true,
        onlyAutomate: true
      }
    }]
  ]
};

GitHub Actions with BrowserStack

yaml
name: BrowserStack Mobile Tests

on:
  push:
    branches: [main, develop]
  pull_request:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM

jobs:
  browserstack-tests:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Upload app to BrowserStack
        id: upload-app
        run: |
          RESPONSE=$(curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \
            -X POST "https://api-cloud.browserstack.com/app-automate/upload" \
            -F "file=@./app/android/app-release.apk")
          
          APP_URL=$(echo $RESPONSE | jq -r '.app_url')
          echo "app_url=$APP_URL" >> $GITHUB_OUTPUT
          echo "Uploaded app: $APP_URL"
      
      - name: Run tests on BrowserStack
        env:
          BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
          BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
          BROWSERSTACK_APP_ID: ${{ steps.upload-app.outputs.app_url }}
        run: npm run test:browserstack
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: |
            allure-results/
            screenshots/
          retention-days: 7
      
      - name: Notify on failure
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "❌ BrowserStack tests failed",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*BrowserStack Tests Failed*\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}\n*Logs:* ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
                  }
                }
              ]
            }

Test Status Marking

Mark tests as passed/failed in BrowserStack dashboard.

javascript
// config/wdio.browserstack.conf.js
exports.config = {
  // ... other config
  
  afterTest: async function (test, context, { error, passed }) {
    // Mark test status in BrowserStack
    await driver.executeScript(
      'browserstack_executor: {"action": "setSessionStatus", "arguments": {"status":"' +
      (passed ? 'passed' : 'failed') +
      '", "reason": "' + (error?.message || 'Test completed') + '"}}'
    );
    
    // Take screenshot on failure
    if (!passed) {
      const timestamp = new Date().getTime();
      await driver.saveScreenshot(`./screenshots/FAILED_${test.title}_${timestamp}.png`);
    }
  }
};

BrowserStack Capabilities Reference

CapabilityDescriptionExample
deviceNameDevice to test on'Samsung Galaxy S23'
osVersionOS version'13.0'
projectNameProject name in dashboard'My Mobile App'
buildNameBuild identifier'v1.2.3'
sessionNameTest session name'Login Tests'
debugEnable debug logstrue
networkLogsCapture network logstrue
videoRecord videotrue
localUse BrowserStack Localfalse
appiumLogsCapture Appium logstrue
deviceLogsCapture device logstrue
geoLocationSet device location'US'
timezoneSet device timezone'New_York'
languageDevice language'en'
localeDevice locale'en_US'

Best Practices

✅ DO

  • Use meaningful build and session names
  • Enable debug logs for troubleshooting
  • Mark test status (passed/failed)
  • Run tests in parallel (max 5-10)
  • Use environment variables for credentials
  • Upload app once, reuse app_url
  • Set appropriate timeouts
  • Clean up old builds regularly

❌ DON'T

  • Hardcode credentials in config
  • Upload app for every test run
  • Run too many parallel sessions
  • Leave video recording always on
  • Ignore BrowserStack session limits
  • Use outdated device/OS versions
  • Skip marking test status
  • Forget to handle timeouts

Debugging Failed Tests

javascript
// Access BrowserStack session details
afterTest: async function (test, context, { error, passed }) {
  const sessionId = await driver.sessionId;
  const sessionUrl = `https://app-automate.browserstack.com/dashboard/v2/sessions/${sessionId}`;
  
  console.log(`BrowserStack Session: ${sessionUrl}`);
  
  if (!passed) {
    console.log(`Test failed: ${test.title}`);
    console.log(`Error: ${error?.message}`);
    console.log(`View logs and video: ${sessionUrl}`);
  }
}

Cost Optimization Tips

💡 Optimize BrowserStack Usage:
  • Disable video recording for passing tests (enable only on failure)
  • Reduce parallel sessions to match your plan limits
  • Use device caching - reuse same device for multiple tests
  • Set idle timeout to release devices quickly
  • Run critical tests only on BrowserStack, others locally
  • Schedule tests during off-peak hours if possible

Package.json Scripts

json
{
  "scripts": {
    "test:local": "wdio config/wdio.android.conf.js",
    "test:browserstack": "wdio config/wdio.browserstack.conf.js",
    "test:browserstack:parallel": "wdio config/wdio.browserstack.parallel.conf.js",
    "test:browserstack:local": "wdio config/wdio.browserstack.local.conf.js",
    "upload:android": "curl -u \"$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY\" -X POST \"https://api-cloud.browserstack.com/app-automate/upload\" -F \"file=@./app/android/app.apk\"",
    "upload:ios": "curl -u \"$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY\" -X POST \"https://api-cloud.browserstack.com/app-automate/upload\" -F \"file=@./app/ios/app.ipa\""
  }
}
15

Reporting

Tooling

Generate comprehensive test reports with Allure.

Allure Reporter Setup

bash
# Install Allure reporter
npm install --save-dev @wdio/allure-reporter

# Generate report
allure generate allure-results --clean -o allure-report
allure open allure-report

Config

javascript
// wdio.conf.js
exports.config = {
  reporters: [
    'spec',
    ['allure', {
      outputDir: 'allure-results',
      disableWebdriverStepsReporting: false,
      disableWebdriverScreenshotsReporting: false
    }]
  ]
};

Adding Test Metadata

javascript
import allure from '@wdio/allure-reporter';

it('should login successfully', async () => {
  allure.addFeature('Authentication');
  allure.addSeverity('critical');
  allure.addStory('User Login');
  
  await LoginScreen.login('user@test.com', 'password');
  
  const screenshot = await driver.takeScreenshot();
  allure.addAttachment('Login Success', Buffer.from(screenshot, 'base64'), 'image/png');
  
  await expect(HomeScreen.welcomeMessage).toBeDisplayed();
});
16

Quick Reference

Tooling

Common Commands

ActionCommandExample
Find Element$(selector)$('~login-button')
Find Elements$$(selector)$$('//android.widget.Button')
Clickelement.click()await loginBtn.click()
Enter Textelement.setValue(text)await input.setValue('test')
Get Textelement.getText()await label.getText()
Waitelement.waitForDisplayed()await el.waitForDisplayed({timeout: 5000})
Swipedriver.execute('mobile: swipe')await driver.execute('mobile: swipe', {direction: 'up'})
Hide Keyboarddriver.hideKeyboard()await driver.hideKeyboard()

Appium Commands

CommandDescription
npx appiumStart Appium server
npx appium -p 4724 --allow-corsStart on specific port with CORS
npx appium driver listList available drivers
npx appium driver install uiautomator2Install Android driver
npx appium driver install xcuitestInstall iOS driver
npx appium doctorCheck Appium setup
adb devicesList Android devices
xcrun simctl listList iOS simulators

Selector Syntax

javascript
// Accessibility ID (Best - Cross-platform)
$('~login-button')

// Android Resource ID
$('android=new UiSelector().resourceId("com.app:id/button")')

// Android Text
$('android=new UiSelector().text("Login")')

// iOS Predicate String
$('-ios predicate string:name == "Login" AND type == "XCUIElementTypeButton"')

// iOS Class Chain
$('-ios class chain:**/XCUIElementTypeButton[`name == "Login"`]')

// XPath (Last Resort)
$('//android.widget.Button[@text="Login"]')
$('//XCUIElementTypeButton[@name="Login"]')
💡 Resources: Visit appium.io/docs for complete documentation.