Environment Setup
SetupAppium is an open-source test automation framework for mobile applications supporting native, hybrid, and mobile web apps on iOS and Android.
Prerequisites
- Node.js (v18 or higher)
- Android Studio (for Android testing)
- Xcode (for iOS testing - macOS only)
- Java JDK (Adoptium OpenJDK 11 or 17)
Android Setup
# 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)
# 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
npx appium doctor to verify your setup.WebDriverIO Config
ConfigWebDriverIO is configured via wdio.conf.js. You can have separate configs for Android and iOS.
wdio.android.conf.js
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
# 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
Locator Strategies
CoreAppium provides multiple ways to locate mobile elements. Prefer accessibility IDs for stability.
Locator Priority
| Priority | Strategy | Example | Use Case |
|---|---|---|---|
| 1 (Best) | Accessibility ID | ~"loginButton" | Most stable, cross-platform |
| 2 | Resource ID | id=com.app:id/username | Android-specific, reliable |
| 3 | Class Chain (iOS) | -ios class chain:**/XCUIElementTypeButton | iOS-specific, performant |
| 4 | XPath | //android.widget.Button[@text="Login"] | Last resort, slower |
Android Locators
// ✅ 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
// ✅ 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]')
Actions & Interactions
ActionsAll Appium actions are async. Always await them.
// 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();
Assertions
AssertionsWebDriverIO uses expect assertions that auto-retry until timeout.
// 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();
Page Object Model
ArchitectureAlways use Page Object Model (POM) for maintainable mobile test automation.
Screen Object Structure
// 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
// 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;
Test Structure
ArchitectureFollow the Arrange-Act-Assert pattern for clear, maintainable tests.
// 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');
});
});
Wait Strategies
Best PracticesNever use hard-coded sleeps! Always use explicit waits.
// ❌ 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' }
);
Mobile Gestures
Best PracticesAppium provides mobile-specific gestures for realistic user interactions.
// 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' }
]);
App State Management
Best PracticesManage app lifecycle for reliable test execution.
// 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');
WebView Context
AdvancedSwitch between native and web contexts for hybrid apps.
// 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');
Hooks & Lifecycle
AdvancedHooks execute code at specific points in the test lifecycle.
// 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');
}
};
CI/CD Integration
ToolingIntegrate Appium tests into your CI/CD pipeline.
GitHub Actions Example
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/
BrowserStack Integration
Cloud TestingBrowserStack provides cloud-based real device testing for mobile applications. Run tests on 3000+ real devices without maintaining physical device labs.
Setup & Authentication
# 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
// 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
# 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
// 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.
// 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
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.
// 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
| Capability | Description | Example |
|---|---|---|
deviceName | Device to test on | 'Samsung Galaxy S23' |
osVersion | OS version | '13.0' |
projectName | Project name in dashboard | 'My Mobile App' |
buildName | Build identifier | 'v1.2.3' |
sessionName | Test session name | 'Login Tests' |
debug | Enable debug logs | true |
networkLogs | Capture network logs | true |
video | Record video | true |
local | Use BrowserStack Local | false |
appiumLogs | Capture Appium logs | true |
deviceLogs | Capture device logs | true |
geoLocation | Set device location | 'US' |
timezone | Set device timezone | 'New_York' |
language | Device language | 'en' |
locale | Device 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
// 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
- 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
{
"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\""
}
}
Reporting
ToolingGenerate comprehensive test reports with Allure.
Allure Reporter Setup
# 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
// wdio.conf.js
exports.config = {
reporters: [
'spec',
['allure', {
outputDir: 'allure-results',
disableWebdriverStepsReporting: false,
disableWebdriverScreenshotsReporting: false
}]
]
};
Adding Test Metadata
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();
});
Quick Reference
ToolingCommon Commands
| Action | Command | Example |
|---|---|---|
| Find Element | $(selector) | $('~login-button') |
| Find Elements | $$(selector) | $$('//android.widget.Button') |
| Click | element.click() | await loginBtn.click() |
| Enter Text | element.setValue(text) | await input.setValue('test') |
| Get Text | element.getText() | await label.getText() |
| Wait | element.waitForDisplayed() | await el.waitForDisplayed({timeout: 5000}) |
| Swipe | driver.execute('mobile: swipe') | await driver.execute('mobile: swipe', {direction: 'up'}) |
| Hide Keyboard | driver.hideKeyboard() | await driver.hideKeyboard() |
Appium Commands
| Command | Description |
|---|---|
npx appium | Start Appium server |
npx appium -p 4724 --allow-cors | Start on specific port with CORS |
npx appium driver list | List available drivers |
npx appium driver install uiautomator2 | Install Android driver |
npx appium driver install xcuitest | Install iOS driver |
npx appium doctor | Check Appium setup |
adb devices | List Android devices |
xcrun simctl list | List iOS simulators |
Selector Syntax
// 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"]')