Your First iOS Test

9 min read

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 -1

Note 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:

StrategyWhen to use
AccessibilityIdElement has accessibilityIdentifier or accessibilityLabel
-ios predicate stringMultiple conditions needed
-ios class chainRelative position in hierarchy
XPathLast 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:

  1. Appium server is running (appium)
  2. The target Simulator is available (xcrun simctl list devices shows it)
  3. The .app path in IOSBaseTest is correct
mvn test -Dtest=IOSCalculatorTest

Watch 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.

// tip to track lessons you complete and pick up where you left off across devices.