← Back to Blog

Getting started with Appium: universal mobile automation

Published December 30, 2025 8 min read
Mobile Testing Appium WebdriverIO iOS Android Automation

// todos

  • purpose of the article - specific
  • i use them to check critical paths (like login) which behaves differently on 3 devices in my case
  • I use this to keep hybrid apps stable across iOS/Android releases, catch regressions early, and reduce flaky releases.
  • rename/adjust “Appium is the best choice” tone to be more evidence-led (reads less like marketing, more like expert judgment)
  • regressions, cut the time after releases
  • images inspector

I struggled with setting up Appium, getting stuck in the planning phase without ever launching my first test. After figuring out the bare minimum needed to get tests running, I wanted to share my findings. This guide gets you running tests quickly with no app build required. You can start by testing system apps like Settings, or automate Safari/Chrome browsers. The goal is simple: just launch it. Once you see Appium in action, the rest becomes clearer.

Appium Logo

Why Appium?

Appium is the best choice for comprehensive mobile automation. While alternatives exist, none match Appium’s combination of power, flexibility, and cross-platform support.

Maestro is simpler to get started with and uses YAML-based test scripts, but it’s less flexible for complex scenarios. Snapshot testing excels at UI regression testing but can’t handle user interactions or complex flows. Appium gives you the full power: write tests once that run on both iOS and Android, access native UI elements and webviews seamlessly, and automate anything on the device, even system apps like Settings or mobile browsers.

Appium is universal: write tests once and run them on both iOS and Android with minor platform-specific adjustments. It provides full access to native UI elements, webviews, and system interactions. You don’t need an app installed to get started - you can automate system settings, Safari, or any other app on the device. It uses the WebDriver protocol, making it familiar to web automation developers.

If you need something simpler, consider Maestro. If you only need UI regression testing, snapshot tests might suffice. But for comprehensive mobile automation that handles native apps, hybrid apps, and system-level interactions, Appium is the clear choice.

Goal: Just Launch the Test

You don’t need a specific app to get started with Appium. You can open system settings and verify a particular setting exists, test Safari behavior on iOS, automate any installed app on the device, or test your own app without building it first (if it’s already installed).

Note on Desktop Web Automation: Appium is designed for mobile automation and is not suitable for desktop web browser testing (e.g., Chrome on macOS). For desktop web automation, use dedicated tools like Playwright or Cypress, which are specifically built for web testing and provide a much better experience for desktop browsers.

However, Appium’s flexibility shines when you need to share testing logic across platforms. You can write shared utility functions and page object models that work with both Appium (for mobile) and Playwright/Cypress (for desktop web). This allows you to maintain a single set of test logic for web elements while using the best tool for each platform - Appium for mobile automation and Playwright/Cypress for desktop web testing.

Here’s a minimal example that opens iOS Settings:

import { remote } from "webdriverio";

const driver = await remote({
  hostname: "localhost",
  port: 4723,
  capabilities: {
    platformName: "iOS",
    "appium:automationName": "XCUITest",
    "appium:deviceName": "iPhone 17 Pro",
    "appium:bundleId": "com.apple.Preferences", // System Settings
  },
});

// Check if a setting exists
const generalSetting = await driver.$("~General");
await generalSetting.waitForDisplayed({ timeout: 5000 });
console.log("General setting found!");

await driver.deleteSession();

For a complete working example with iOS and Android tests, see the appium-example project.

WebdriverIO vs Appium: Understanding the Difference

This is a common source of confusion. WebdriverIO is a Node.js client library that implements the WebDriver protocol. Appium is a server that implements the WebDriver protocol for mobile platforms.

You can use Appium without WebdriverIO (using other clients like Python’s Appium-Python-Client), but WebdriverIO is the most popular choice in the JavaScript ecosystem.

In practice, Appium is the server that controls mobile devices, while WebdriverIO is the JavaScript client that talks to Appium (or any WebDriver server).

Here’s how they work together:

import { remote } from "webdriverio";

// WebdriverIO connects to Appium server
const driver = await remote({
  hostname: "localhost", // Appium server address
  port: 4723, // Appium default port
  capabilities: {
    platformName: "iOS",
    "appium:automationName": "XCUITest",
    // ... more Appium-specific capabilities
  },
});

Best Advantage: Hybrid Mode

Appium’s most powerful feature is hybrid mode - the ability to switch between native and web contexts seamlessly. This is essential for apps built with Capacitor, React Native WebView, or similar frameworks.

// Switch to native context
await driver.switchContext("NATIVE_APP");

// Click a native button
const nativeButton = await driver.$("~Settings");
await nativeButton.click();

// Switch to webview context
// Using returnDetailedContexts: true returns detailed information including URLs,
// making it much easier to identify the correct webview context
const contexts = await driver.getContexts({
  returnDetailedContexts: true,
});
const webviewContext = contexts.find((ctx) =>
  ctx.url?.startsWith("capacitor://")
);
await driver.switchContext(webviewContext.id);

// Now interact with web elements
const webButton = await driver.$("button.submit");
await webButton.click();

Without returnDetailedContexts: true, you only get context IDs, making it harder to distinguish between multiple webviews.

This hybrid capability makes Appium uniquely powerful for modern mobile apps that blend native and web technologies.

That said, hybrid mode isn’t smooth sailing. I spent a week trying to get a simple Capacitor app working with context switching, and I’m still hitting edge cases. Sometimes switchContext() works perfectly, other times it fails silently or throws cryptic errors.

Android Hybrid Mode: ChromeDriver Considerations

When testing hybrid Android apps (apps with WebViews) or Chrome browser on Android, you may encounter ChromeDriver compatibility issues. Appium sometimes struggles to automatically manage ChromeDriver versions. I’m still having some issues with chromedrivers, but automatic downloads helps a lot.

Solution 1: Enable Automatic ChromeDriver Download (Recommended)

Appium can automatically download the matching ChromeDriver version. To enable this, start Appium server with the ChromeDriver auto-download flag:

appium --allow-insecure uiautomator2:chromedriver_autodownload

No additional capabilities are needed in your driver configuration. The server flag is sufficient to enable automatic ChromeDriver downloads.

Solution 2: Manual ChromeDriver Download

If automatic download doesn’t work, download ChromeDriver manually from Chrome for Testing to match your installed Chrome version:

  1. Check your Android device/emulator’s Chrome version:
adb shell dumpsys package com.android.chrome | grep versionName

Note: This command may return two versions - use the higher version (the lower is often the factory version).

  1. Download the matching ChromeDriver from the Chrome for Testing site
  2. Specify the path in your capabilities:
const driver = await remote({
  capabilities: {
    platformName: "Android",
    "appium:automationName": "UiAutomator2",
    "appium:chromedriverExecutable": "/path/to/chromedriver", // Manual ChromeDriver path
    "appium:appPackage": "com.example.app",
  },
});

iOS Hybrid Mode: Safari in WebViews

For iOS hybrid apps, you may need to explicitly include Safari in webviews to access all WebView contexts. Some hybrid apps use Safari-based WebViews that aren’t detected by default:

const driver = await remote({
  capabilities: {
    platformName: "iOS",
    "appium:automationName": "XCUITest",
    "appium:deviceName": "iPhone 17 Pro",
    "appium:includeSafariInWebviews": true, // Include Safari-based WebViews
    "appium:bundleId": "com.example.app",
  },
});

This capability ensures Appium can detect and switch to Safari-based WebView contexts, which is common in Capacitor and some React Native WebView implementations.

Shadow DOM Limitation on iOS: Safari/WebKit doesn’t support native shadow DOM WebDriver commands on iOS, as documented in this GitHub issue.

Using the Inspector

The Appium Inspector is essential for finding element selectors. Download it from the Appium Inspector repository.

The Inspector connects to your running Appium server and lets you explore the app’s element tree:

  1. Start your Appium server: appium
  2. Launch the Appium Inspector (standalone app)
  3. Enter your server details and desired capabilities
  4. Click “Start Session”
  5. The Inspector will show the element tree and let you record interactions

Inspector Configuration

You need to provide capabilities (similar to your test code) to launch the Inspector. Here’s an example configuration for iOS:

{
  "platformName": "iOS",
  "appium:automationName": "XCUITest",
  "appium:deviceName": "iPhone 17 Pro",
  "appium:platformVersion": "26.2", // iOS 26
  "appium:bundleId": "com.apple.Preferences"
}

Save your capability JSON files somewhere accessible as this speeds up development significantly. Instead of manually entering capabilities each time, you can load them directly in the Inspector. You’ll need to provide these parameters every time you launch a session, so having them saved is a huge time-saver.

Biggest Disadvantage: Rough and Sturdy

Appium is not polished. It’s a powerful but rough tool. Mobile automation with appium is inherently unstable due to animations, timing, and device state, making tests flaky by nature. Appium requires more code than simpler tools, and when something fails, it’s often unclear why. iOS and Android behave differently, requiring platform-specific workarounds.

My current pain points:

  • Capacitor hybrid mode: A week of setup and context switching still breaks unpredictably
  • iOS Shadow DOM: Webdriver has issues accessing shadow roots, forcing hacky workarounds
  • Android ChromeDriver: Bidi warnings, version mismatches, and WebView quirks

Despite the frustrations, I still believe Appium is the best tool for comprehensive mobile automation. I’ll be posting more findings as I continue working through these issues.

Test Organization: Mocha and Assertions

Appium itself provides minimal test organization. You’ll want to use a test framework:

Mocha is a popular choice (and what I use):

import assert from "node:assert";

describe("Login tests", function () {
  let driver: WebdriverIO.Browser;

  this.timeout(60000); // Appium operations need longer timeouts

  before(async function () {
    driver = await getDriver();
  });

  after(async function () {
    await driver.deleteSession();
  });

  it("should login successfully", async function () {
    await loginUser(driver, email, password);
    const header = await driver.$("h2");
    await header.waitForDisplayed({ timeout: 5000 });
    assert.strictEqual(await header.getText(), "Hello, User 👋");
  });
});

Node’s built-in assert works fine, but you can also use Chai for more expressive assertions, Jest if you prefer its ecosystem, or Vitest for faster execution.

The key is that Appium doesn’t care - it just provides the WebDriver client. You organize tests however you want.

Note on device startup: The initial boot of a simulator or emulator can take longer than your test timeout (especially on first launch). If your test times out during device startup, simply run it again as subsequent launches are much faster once the device is already booted.

Error Handling: Connection Failures

Connection failures are common when working with Appium: the server might not be running, the connection might timeout, or network issues could occur. Handle these gracefully to improve test reliability and developer experience.

Here’s a simple example that handles connection failures:

import { getIOSDriver } from "./utils/driver.js";

try {
  const driver = await getIOSDriver();
  // Connection successful - proceed with tests
  // ... your test code here ...
  await driver.deleteSession();
} catch (error) {
  // Check if it's a connection error
  const err = error as NodeJS.ErrnoException;
  if (err.code === "ECONNREFUSED") {
    console.error(
      "Appium server is not running. Start it with: npm run appium"
    );
  } else {
    console.error(`Connection error: ${error.message}`);
  }
}

I’ve included example error handling in appium-example project’s error-handling.test.ts and utils/error-handling.ts files.

Ready example

For a complete setup example, check out the appium-example project, which includes iOS and Android Settings app tests, Safari and Chrome browser tests, error handling examples for connection failures, test organization with Mocha, and driver setup utilities.

This example demonstrates Appium’s flexibility - you can test system apps and mobile browsers without building your own app, making it perfect for learning and experimentation.

iOS setup tip: If connecting to the iOS simulator takes too long, you can manually open the WebDriverAgent app on the simulator to speed up the connection. The WebDriverAgent app is automatically installed by Appium when you first connect to a device.