← Back to Blog

Getting started with Appium: universal mobile automation

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

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 is for getting your first useful mobile automation test running quickly, even if you do not have a dedicated test app ready. You can start by testing system apps like Settings, or automate Safari and Chrome on mobile devices. From there, you can move toward the tests that usually matter most in real products: critical paths such as login, onboarding, payments, or other flows that behave differently across iOS, Android, simulators, emulators, and physical devices.

I use this kind of setup to keep hybrid apps stable across iOS and Android releases, catch regressions earlier, and make release checks less manual. The goal is simple: just launch it. Once you see Appium in action, the rest becomes clearer.

Appium Logo

Why Appium?

Appium is a strong choice when you need broad, code-driven mobile automation across native apps, hybrid apps, mobile browsers, and system-level interactions. Its main advantage is not that it is the easiest tool to start with. It is that it gives you a WebDriver-based way to automate real mobile surfaces across iOS and Android.

Maestro is simpler to get started with and uses YAML-based flows, which can be a better fit for straightforward end-to-end scenarios. Snapshot testing is useful for visual regressions, but it does not cover user interactions or complex flows by itself. Appium sits in a different place: it lets you write automation in a real programming language, access native UI elements and webviews, and automate surfaces such as Settings or mobile browsers.

Appium is especially useful when you want one testing approach for native and hybrid mobile behavior. You can share structure between iOS and Android tests, then keep platform-specific adjustments where the platforms genuinely differ. You also do not need your own app installed to learn the basics - you can automate system settings, Safari, Chrome, or any installed app on the device. Because it uses the WebDriver protocol, many concepts feel familiar if you have worked with browser automation before.

If you need a simpler smoke-test tool, consider Maestro. If you only need visual regression coverage, snapshot tests might be enough. If you need programmable automation that can move between native views, webviews, mobile browsers, and device surfaces, Appium is worth the extra setup cost.

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.