ScreenshotNeo

BlogHow-to

How to Write Appium Tests for iOS

Set up Appium’s XCUITest driver, create an iOS session, and write a runnable test for a Simulator or real iPhone, with setup and troubleshooting guidance.

By the ScreenshotNeo team4 October 20269 min read

To write Appium tests for iOS, install Appium and its XCUITest driver on a macOS host with Xcode, start the Appium server, then create a session with platformName: iOS, appium:automationName: XCUITest, and an app target. In the test, find a control, interact with it, assert the resulting state, and always end the session. The example below uses JavaScript and the official Appium WebdriverIO client against an iOS Simulator.

1. Understand the iOS automation stack

Appium provides a WebDriver interface to your test. Its official iOS driver is XCUITest. The driver communicates through WebDriverAgent (WDA), which uses Apple’s XCTest internals to automate the app on a Simulator or real device. This lets your test use an Appium client while UI automation runs through Apple’s iOS testing stack. See Appium’s driver catalog, the driver architecture overview, and the XCUITest overview.

2. Prepare the host and install Appium

The standard setup uses macOS and Xcode or its developer tools. A Simulator-first setup is usually the simplest way to get a first session running: it avoids device trust and provisioning steps. Check the XCUITest driver’s live setup and system-requirements documentation for compatibility between your Appium, driver, Xcode, and iOS versions; the version references can change.

  1. Install Node.js and the Xcode version appropriate for your project.
  2. Install Appium and the XCUITest driver.
  3. Start Appium and verify that it lists the installed driver.
  4. Install the app in a Simulator or provide a build package that Appium can install.
npm install --global appium
appium driver install xcuitest
appium driver list --installed
appium

Keep the server running in its terminal. By default, the examples below connect to http://127.0.0.1:4723. Follow the XCUITest setup guide and driver installation guide for the current prerequisites and installation details.

3. Choose Simulator or real iPhone

Target Good starting point when Extra setup
iOS Simulator You need a quick local feedback loop and do not need physical hardware behavior for every test. Select an installed Simulator runtime and device. Provide an app build or installed app bundle ID.
Real iPhone You need coverage on physical hardware or need to validate device-specific behavior. Trust the host, enable Developer Mode on iOS/iPadOS 16 and later, enable UI Automation, and provision WDA with a valid profile.

For a physical target, specify its UDID with appium:udid. Safari webview automation also requires Web Inspector and Remote Automation settings. Read the real-device preparation guide before debugging a device session. Zoom and other accessibility settings can change exposed elements or coordinate behavior; inspect the Appium page source and logs if an element seems missing.

There is a documented Windows/Linux route, but it has narrower constraints: real devices only, iOS/tvOS 18 or later, no automatic device selection, and no default xcodebuild-based WDA startup. Do not assume it matches the macOS workflow; use the non-macOS host guide for its RemoteXPC-specific requirements.

4. Set session capabilities

Capabilities are session-start settings; you cannot change them after the session starts. Appium requires platformName and appium:automationName. XCUITest also needs an app path, bundle identifier, or browser target. Appium-specific capabilities use the appium: prefix. For a local app package, set appium:app; for an app already installed on the target, use appium:bundleId. Use a device name to select a Simulator, and a UDID for a physical device or parallel runs.

{
  "platformName": "iOS",
  "appium:automationName": "XCUITest",
  "appium:deviceName": "iPhone Simulator",
  "appium:app": "/absolute/path/to/MyApp.app"
}

Replace the example path with a real installable build. A real device session generally also specifies appium:udid. See the Appium capabilities guide and the XCUITest capabilities reference for available settings and current semantics.

5. Write and run a JavaScript test

This example uses Appium’s JavaScript client via WebdriverIO. It assumes the app exposes an accessibility identifier loginButton and that pressing it reveals an element with identifier welcomeMessage. Replace these with identifiers and expected behavior from your app. Install the client in a project with npm install webdriverio, save the following as ios-test.mjs, set IOS_APP to your app path, and run node ios-test.mjs while the Appium server is running.

import { remote } from 'webdriverio';
import assert from 'node:assert/strict';

const appPath = process.env.IOS_APP;
if (!appPath) throw new Error('Set IOS_APP to an absolute path to a .app or .ipa');

const driver = await remote({
  hostname: '127.0.0.1',
  port: 4723,
  path: '/',
  capabilities: {
    platformName: 'iOS',
    'appium:automationName': 'XCUITest',
    'appium:deviceName': process.env.IOS_DEVICE_NAME ?? 'iPhone Simulator',
    'appium:app': appPath,
    'appium:newCommandTimeout': 120
  }
});

try {
  const loginButton = await driver.$('~loginButton');
  await loginButton.waitForDisplayed({ timeout: 10000 });
  await loginButton.click();

  const welcomeMessage = await driver.$('~welcomeMessage');
  await welcomeMessage.waitForDisplayed({ timeout: 10000 });
  assert.equal(await welcomeMessage.isDisplayed(), true);
} finally {
  await driver.deleteSession();
}

The ~ selector in WebdriverIO targets an accessibility identifier. Prefer stable accessibility identifiers exposed by the app over labels that may change with localization or coordinates that depend on layout. The specific locator and client API should follow your installed client version; Appium’s capabilities guide documents session setup, while the XCUITest documentation covers driver behavior.

Run against an installed app

If the app is already installed, configure appium:bundleId in place of appium:app. Ensure the identifier matches the app’s actual bundle identifier and that the target device has the app installed.

'appium:bundleId': 'com.example.myapp'

Keep tests independent and readable

  • Give each test a clear starting state and expected outcome.
  • Wait for a meaningful element state instead of sleeping for a fixed duration wherever possible.
  • Use accessibility identifiers as test hooks and keep them stable.
  • End the session in a finally block so assertion failures do not leave WDA sessions running.
  • Keep capabilities in environment-specific configuration when switching between Simulator and device.

6. Run the same session with cURL, Python, or Node.js

These examples create an Appium session directly through its WebDriver HTTP endpoint. They demonstrate session creation and cleanup; a real test client is generally more convenient for element lookup and assertions. Run them against the same local server and adjust the app path and device selection.

cURL

curl -sS -X POST http://127.0.0.1:4723/session \
  -H 'Content-Type: application/json' \
  -d '{"capabilities":{"alwaysMatch":{"platformName":"iOS","appium:automationName":"XCUITest","appium:deviceName":"iPhone Simulator","appium:app":"/absolute/path/to/MyApp.app"}}}'

Save the returned sessionId, use it in WebDriver commands to interact with the session, then delete it with DELETE /session/{sessionId}.

Python client

Install the Appium Python client with python -m pip install Appium-Python-Client. This runnable example starts a session, taps an accessibility ID, checks visibility, and quits. Replace the sample IDs with those exposed by your app.

import os
from appium import webdriver
from appium.options.ios import XCUITestOptions
from appium.webdriver.common.appiumby import AppiumBy

app_path = os.environ.get("IOS_APP")
if not app_path:
    raise RuntimeError("Set IOS_APP to an absolute path to a .app or .ipa")

options = XCUITestOptions()
options.platform_name = "iOS"
options.automation_name = "XCUITest"
options.device_name = os.environ.get("IOS_DEVICE_NAME", "iPhone Simulator")
options.app = app_path

driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
try:
    button = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "loginButton")
    button.click()
    message = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "welcomeMessage")
    assert message.is_displayed()
finally:
    driver.quit()

Node.js client

The WebdriverIO example above is the Node.js test-client version. For a concise session-only check using the raw HTTP API, Node’s built-in fetch can create a session and then delete it:

const capabilities = {
  alwaysMatch: {
    platformName: 'iOS',
    'appium:automationName': 'XCUITest',
    'appium:deviceName': process.env.IOS_DEVICE_NAME ?? 'iPhone Simulator',
    'appium:app': process.env.IOS_APP
  }
};
if (!capabilities.alwaysMatch['appium:app']) throw new Error('Set IOS_APP');

const created = await fetch('http://127.0.0.1:4723/session', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ capabilities })
});
const result = await created.json();
if (!created.ok) throw new Error(JSON.stringify(result));
try {
  console.log('Created Appium session:', result.sessionId);
} finally {
  await fetch(`http://127.0.0.1:4723/session/${result.sessionId}`, { method: 'DELETE' });
}

7. Make tests reliable and diagnose failures

Symptom Likely cause What to check or change
Driver is missing or Appium rejects the automation name The XCUITest driver was not installed or did not load. Run appium driver list --installed, install with appium driver install xcuitest, then restart the server.
Session creation fails before the app opens Invalid capability, app path, bundle ID, Simulator selection, or incompatible local tool versions. Check required capability names and the appium: namespace; confirm the package exists and matches the target. Verify versions against the current driver requirements.
WDA build, signing, or launch fails on a real iPhone Device trust, Developer Mode, UI Automation, or WDA provisioning is incomplete. Follow the real-device preparation steps, trust the Mac, enable required settings, and configure a valid WDA provisioning profile.
Element lookup times out The app state differs from the test expectation, the identifier is wrong, or the element is not exposed to accessibility. Inspect page source and server logs, verify the accessibility identifier, wait for the correct state, and check relevant accessibility settings such as Zoom.
Tap reaches the wrong area or coordinates drift Coordinates are brittle across device sizes, display settings, and layouts. Use an accessibility identifier and element interaction instead of hard-coded coordinates; inspect device accessibility settings.
Safari webview is unavailable Web Inspector or Remote Automation is disabled on the device. Enable both as described in the device-preparation documentation.
Tests pass alone but fail in a suite Sessions or app state leak across tests, or parallel workers target the same device. Quit every session in cleanup, reset app state deliberately, and assign a distinct UDID to each physical-device worker.

When investigating a failure, separate server startup, session creation, WDA launch, app launch, and element interaction. The first failing layer narrows the cause. Preserve Appium server logs and the page source around the failure; avoid treating every missing element as an app defect.

8. Performance, reliability, and cost considerations

  • Startup time: Session creation includes target preparation and WDA startup, so reuse a session for related steps within a test where appropriate. Always clean it up at the end.
  • Waiting: Condition-based waits make tests resilient to normal rendering delays. Fixed sleeps lengthen every run and still may be too short under load.
  • Parallelism: Parallel runs need separate target devices or Simulators and unambiguous device selection. For real-device runs, set a UDID rather than relying on automatic selection.
  • Maintenance: Stable accessibility identifiers and assertions on user-visible outcomes reduce fragility. Keep driver, Appium, Xcode, and iOS versions recorded and check the live compatibility documentation before upgrades.
  • Cost: Simulator runs use your Mac and installed development tools. Physical-device coverage requires access to iPhones and setup time for trust and signing. A Mac is relevant for the standard Xcode workflow; an iPhone is optional when your coverage needs can be met by a Simulator.

9. Capture screenshots of web pages used in your test workflow

Appium is for automating native or web app interfaces on iOS. If a test workflow also needs a screenshot of a website as a fixture, report, or visual reference, a website screenshot API can handle that separate capture task. ScreenshotNeo is a website screenshot API and MCP server for developers. It returns PNG, JPEG, WebP, or PDF from one GET request and supports custom waits, headers, cookies, device presets, and other capture controls.

Or skip the browser setup

For a website capture, make one request to ScreenshotNeo. See the ScreenshotNeo API documentation for the full options and response details.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers say the page verdict and billing status.
  • An MCP server lets Claude, Cursor, and other MCP clients take screenshots, get page information, and capture PDFs.
  • 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.

Sign up for 1,000 free screenshots a month, with no card required.

10. Frequently asked questions

Do I need to write iOS tests in Swift?

No. Appium tests can use an Appium client in the language your team uses; the driver uses XCTest through WDA on the target.

Can Appium test Safari as well as an installed app?

XCUITest supports browser targets, but Safari web automation on a real device has additional Web Inspector and Remote Automation requirements.

Can a Simulator replace all physical-device testing?

No single target covers every need. Use Simulators for convenient local coverage and add physical devices for behaviors that require hardware or a real iOS environment.

Can I change capabilities after the session starts?

No. End the session and create another with the capabilities for the new target or configuration.