iOS tests follow the same structure as Android tests — @BeforeClass, driver creation, locators, assertions, @AfterClass — but the capabilities, the driver class, and the locator strategies are different. This lesson writes a complete first iOS test targeting the iOS Simulator using the TestApp sample bundled with the XCUITest driver.
The app under test
Appium's XCUITest driver ships with a TestApp.app Simulator bundle in its npm package. Find it:
find ~/.appium -name "TestApp.app" 2>/dev/null | head -1Note the absolute path — you will use it as the app capability.
Alternatively, any .app bundle you have locally works. The UIKitCatalog sample from Apple's developer resources is another good option.
iOS BaseTest
Create src/test/java/com/qa/base/IOSBaseTest.java:
package com.qa.base;
import io.appium.java_client.ios.IOSDriver;
import io.appium.java_client.ios.options.XCUITestOptions;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
public class IOSBaseTest {
protected IOSDriver driver;
@BeforeClass
public void setUp() throws MalformedURLException {
XCUITestOptions options = new XCUITestOptions()
.setDeviceName("iPhone 15 Pro")
.setPlatformVersion("17.2")
.setApp("/Users/yourname/.appium/node_modules/appium-xcuitest-driver/node_modules/appium-ios-simulator/test/assets/TestApp/build/Release-iphonesimulator/TestApp.app")
.setNewCommandTimeout(Duration.ofSeconds(60));
driver = new IOSDriver(new URL("http://127.0.0.1:4723"), options);
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
}
@AfterClass
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
}The deviceName must match a Simulator listed by xcrun simctl list devices. If the Simulator is not already running, Appium boots it automatically.
Writing the iOS test
Create src/test/java/com/qa/tests/IOSCalculatorTest.java:
package com.qa.tests;
import com.qa.base.IOSBaseTest;
import io.appium.java_client.AppiumBy;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.time.Duration;
public class IOSCalculatorTest extends IOSBaseTest {
@Test
public void verifyAppLaunches() {
// TestApp shows a simple interface with a "Compute Sum" button
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
WebElement computeButton = wait.until(
ExpectedConditions.visibilityOfElementLocated(
AppiumBy.accessibilityId("ComputeSumButton")
)
);
Assert.assertTrue(computeButton.isDisplayed(), "Compute Sum button should be visible");
}
@Test
public void computeSum() {
// Enter values in the two number fields
WebElement firstField = driver.findElement(AppiumBy.accessibilityId("IntegerA"));
firstField.clear();
firstField.sendKeys("5");
WebElement secondField = driver.findElement(AppiumBy.accessibilityId("IntegerB"));
secondField.clear();
secondField.sendKeys("3");
// Tap the compute button
driver.findElement(AppiumBy.accessibilityId("ComputeSumButton")).click();
// Read the result
String result = driver.findElement(AppiumBy.accessibilityId("Answer")).getText();
Assert.assertEquals(result, "8", "Sum of 5 and 3 should be 8");
}
@Test
public void dismissKeyboard() {
WebElement field = driver.findElement(AppiumBy.accessibilityId("IntegerA"));
field.click();
field.sendKeys("10");
// Dismiss the iOS keyboard
driver.hideKeyboard();
// Verify the keyboard is gone and the field retained the value
Assert.assertEquals(field.getText(), "10");
}
}Key iOS locator differences
Accessibility ID on iOS maps to the element's accessibilityIdentifier. Developers set this in Xcode:
button.accessibilityIdentifier = "ComputeSumButton"If accessibilityIdentifier is not set, Appium falls back to accessibilityLabel. Both are retrieved with AppiumBy.accessibilityId().
No resource-id on iOS. There is no equivalent of Android's resource-id. Your options are:
| Strategy | When to use |
|---|---|
AccessibilityId | Element has accessibilityIdentifier or accessibilityLabel |
-ios predicate string | Multiple conditions needed |
-ios class chain | Relative position in hierarchy |
| XPath | Last resort |
iOS NSPredicate strings
When Accessibility ID is not available, NSPredicate strings give you SQL-like queries against the element tree:
// Find a button with label "Sign In"
driver.findElement(
AppiumBy.iOSNsPredicateString("type == 'XCUIElementTypeButton' AND label == 'Sign In'")
);
// Find any element with value containing "error"
driver.findElement(
AppiumBy.iOSNsPredicateString("value CONTAINS 'error'")
);iOS class chains
Class chains are more specific than XPath and faster than NSPredicate for hierarchical queries:
// Find the third cell in a table
driver.findElement(
AppiumBy.iOSClassChain("**/XCUIElementTypeTable/XCUIElementTypeCell[3]")
);Hiding the keyboard
On iOS, driver.hideKeyboard() dismisses the keyboard. On Android, press the back key:
// Android keyboard dismiss
driver.pressKey(new KeyEvent(AndroidKey.BACK));Running the test
Make sure:
- Appium server is running (
appium) - The target Simulator is available (
xcrun simctl list devicesshows it) - The
.apppath inIOSBaseTestis correct
mvn test -Dtest=IOSCalculatorTestWatch the Simulator — the app installs and launches automatically. All three tests should pass in about 30 seconds.
The shared driver API
Notice how similar the iOS test looks to the Android test. The driver creation uses IOSDriver instead of AndroidDriver, the capabilities use XCUITestOptions instead of UiAutomator2Options, and some locators are iOS-specific — but the overall shape of the test, the assertion style, and the @BeforeClass/@AfterClass lifecycle are identical. This is the W3C WebDriver protocol doing its job.