Mobile Test Automation
Master iOS and Android app testing with modern mobile automation frameworks
Overview
Mobile test automation involves testing applications on iOS and Android devices to ensure functionality, performance, and user experience across different devices, OS versions, and screen sizes. This module covers native app testing, cross-platform testing, and mobile-specific testing challenges.
Key Areas: Native app automation, cross-platform testing, device cloud integration, mobile performance testing, and gesture-based interactions.
iOS Testing
Native iOS app automation with XCUITest and Appium
Android Testing
Android app testing with Espresso and Appium
Cross-Platform
Unified testing approach for both platforms
Cloud Testing
Device cloud and real device testing
Native App Testing with Appium
Appium is the most popular cross-platform mobile automation framework supporting both iOS and Android:
Basic Appium Setup and Tests
// Appium with JavaScript (WebdriverIO)
const { remote } = require('webdriverio');
describe('Mobile App Tests', () => {
let driver;
before(async () => {
// iOS Configuration
const iOSCapabilities = {
platformName: 'iOS',
platformVersion: '16.0',
deviceName: 'iPhone 14',
app: '/path/to/your/app.ipa',
automationName: 'XCUITest',
noReset: false,
fullReset: true
};
// Android Configuration
const androidCapabilities = {
platformName: 'Android',
platformVersion: '13.0',
deviceName: 'Pixel 7',
app: '/path/to/your/app.apk',
automationName: 'UiAutomator2',
appPackage: 'com.example.app',
appActivity: '.MainActivity',
noReset: false,
fullReset: true
};
driver = await remote({
protocol: 'http',
hostname: 'localhost',
port: 4723,
path: '/wd/hub/',
capabilities: iOSCapabilities // or androidCapabilities
});
});
after(async () => {
if (driver) {
await driver.deleteSession();
}
});
it('should login successfully', async () => {
// Wait for login screen
const usernameField = await driver.$('~username-input');
const passwordField = await driver.$('~password-input');
const loginButton = await driver.$('~login-button');
await usernameField.waitForDisplayed({ timeout: 10000 });
// Fill login form
await usernameField.setValue('testuser@example.com');
await passwordField.setValue('password123');
await loginButton.click();
// Verify successful login
const homeScreen = await driver.$('~home-screen');
await homeScreen.waitForDisplayed({ timeout: 10000 });
expect(await homeScreen.isDisplayed()).toBe(true);
});
it('should handle touch gestures', async () => {
// Tap gesture
const button = await driver.$('~tap-button');
await button.click();
// Swipe gesture
const carousel = await driver.$('~image-carousel');
await carousel.waitForDisplayed();
// Swipe left
await driver.touchAction([
{ action: 'press', x: 300, y: 400 },
{ action: 'wait', ms: 1000 },
{ action: 'moveTo', x: 100, y: 400 },
{ action: 'release' }
]);
// Long press
const longPressElement = await driver.$('~long-press-item');
await driver.touchAction([
{ action: 'longPress', element: longPressElement, duration: 2000 }
]);
// Verify context menu appeared
const contextMenu = await driver.$('~context-menu');
expect(await contextMenu.isDisplayed()).toBe(true);
});
it('should test form interactions', async () => {
// Navigate to form screen
const formTab = await driver.$('~form-tab');
await formTab.click();
// Text input
const nameField = await driver.$('~name-field');
await nameField.setValue('John Doe');
expect(await nameField.getValue()).toBe('John Doe');
// Dropdown selection (iOS Picker / Android Spinner)
const countryPicker = await driver.$('~country-picker');
await countryPicker.click();
if (driver.isIOS) {
// iOS Picker Wheel
await driver.$('~United States').click();
await driver.$('~Done').click();
} else {
// Android Spinner
await driver.$('android=new UiSelector().text("United States")').click();
}
// Switch toggle
const notificationSwitch = await driver.$('~notification-switch');
await notificationSwitch.click();
expect(await notificationSwitch.getAttribute('value')).toBe('1');
// Submit form
const submitButton = await driver.$('~submit-form');
await submitButton.click();
// Verify success message
const successMessage = await driver.$('~success-message');
await successMessage.waitForDisplayed({ timeout: 5000 });
expect(await successMessage.getText()).toContain('Form submitted successfully');
});
it('should test device-specific features', async () => {
// Test device rotation
await driver.setOrientation('LANDSCAPE');
// Verify layout adapts to landscape
const headerTitle = await driver.$('~header-title');
const headerHeight = await headerTitle.getSize('height');
expect(headerHeight).toBeLessThan(100); // Compact header in landscape
await driver.setOrientation('PORTRAIT');
// Test background/foreground behavior
await driver.background(3); // Put app in background for 3 seconds
// Verify app state after returning
const currentActivity = await driver.getCurrentActivity();
expect(currentActivity).toContain('MainActivity');
// Test deep linking (Android)
if (driver.isAndroid) {
await driver.startActivity('com.example.app', '.DeepLinkActivity',
'android.intent.action.VIEW', 'myapp://profile/123');
const profileScreen = await driver.$('~profile-screen');
await profileScreen.waitForDisplayed({ timeout: 5000 });
expect(await profileScreen.isDisplayed()).toBe(true);
}
});
it('should test scrolling and list interactions', async () => {
// Navigate to list screen
const listTab = await driver.$('~list-tab');
await listTab.click();
// Wait for list to load
const itemList = await driver.$('~item-list');
await itemList.waitForDisplayed();
// Scroll to find specific item
let targetItem;
let scrollAttempts = 0;
const maxScrolls = 5;
while (scrollAttempts < maxScrolls) {
try {
targetItem = await driver.$('~item-special');
if (await targetItem.isDisplayed()) {
break;
}
} catch (e) {
// Element not found, continue scrolling
}
// Scroll down
await driver.touchAction([
{ action: 'press', x: 200, y: 600 },
{ action: 'wait', ms: 1000 },
{ action: 'moveTo', x: 200, y: 200 },
{ action: 'release' }
]);
scrollAttempts++;
}
if (targetItem) {
await targetItem.click();
// Verify item details screen
const itemDetails = await driver.$('~item-details');
await itemDetails.waitForDisplayed({ timeout: 5000 });
expect(await itemDetails.isDisplayed()).toBe(true);
}
});
});
// Appium with Java and TestNG
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.ios.IOSDriver;
import io.appium.java_client.MobileElement;
import io.appium.java_client.TouchAction;
import io.appium.java_client.touch.WaitOptions;
import io.appium.java_client.touch.offset.PointOption;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.testng.annotations.*;
import java.net.URL;
import java.time.Duration;
public class MobileAppTest {
private AppiumDriver driver;
@BeforeClass
public void setUp() throws Exception {
DesiredCapabilities caps = new DesiredCapabilities();
// Common capabilities
caps.setCapability("noReset", false);
caps.setCapability("fullReset", true);
caps.setCapability("newCommandTimeout", 300);
// Platform-specific setup
if (System.getProperty("platform", "android").equals("android")) {
setupAndroidDriver(caps);
} else {
setupIOSDriver(caps);
}
}
private void setupAndroidDriver(DesiredCapabilities caps) throws Exception {
caps.setCapability("platformName", "Android");
caps.setCapability("platformVersion", "13.0");
caps.setCapability("deviceName", "Pixel_7_API_33");
caps.setCapability("app", System.getProperty("user.dir") + "/apps/app-debug.apk");
caps.setCapability("automationName", "UiAutomator2");
caps.setCapability("appPackage", "com.example.app");
caps.setCapability("appActivity", ".MainActivity");
driver = new AndroidDriver<>(new URL("http://localhost:4723/wd/hub"), caps);
}
private void setupIOSDriver(DesiredCapabilities caps) throws Exception {
caps.setCapability("platformName", "iOS");
caps.setCapability("platformVersion", "16.0");
caps.setCapability("deviceName", "iPhone 14");
caps.setCapability("app", System.getProperty("user.dir") + "/apps/app.ipa");
caps.setCapability("automationName", "XCUITest");
caps.setCapability("bundleId", "com.example.app");
driver = new IOSDriver<>(new URL("http://localhost:4723/wd/hub"), caps);
}
@AfterClass
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
@Test
public void testSuccessfulLogin() {
// Wait for and interact with login elements
MobileElement usernameField = driver.findElementByAccessibilityId("username-input");
MobileElement passwordField = driver.findElementByAccessibilityId("password-input");
MobileElement loginButton = driver.findElementByAccessibilityId("login-button");
usernameField.sendKeys("testuser@example.com");
passwordField.sendKeys("password123");
loginButton.click();
// Verify successful login
MobileElement homeScreen = driver.findElementByAccessibilityId("home-screen");
WebDriverWait wait = new WebDriverWait(driver, 10);
wait.until(ExpectedConditions.visibilityOf(homeScreen));
assertTrue(homeScreen.isDisplayed());
}
@Test
public void testTouchGestures() {
TouchAction touchAction = new TouchAction(driver);
// Tap gesture
MobileElement tapButton = driver.findElementByAccessibilityId("tap-button");
touchAction.tap(PointOption.point(tapButton.getCenter())).perform();
// Swipe gesture - left swipe on carousel
MobileElement carousel = driver.findElementByAccessibilityId("image-carousel");
touchAction
.press(PointOption.point(carousel.getCenter().x + 100, carousel.getCenter().y))
.waitAction(WaitOptions.waitOptions(Duration.ofSeconds(1)))
.moveTo(PointOption.point(carousel.getCenter().x - 100, carousel.getCenter().y))
.release()
.perform();
// Long press gesture
MobileElement longPressElement = driver.findElementByAccessibilityId("long-press-item");
touchAction
.longPress(PointOption.point(longPressElement.getCenter()))
.waitAction(WaitOptions.waitOptions(Duration.ofSeconds(2)))
.release()
.perform();
// Verify context menu appeared
MobileElement contextMenu = driver.findElementByAccessibilityId("context-menu");
assertTrue(contextMenu.isDisplayed());
}
@Test
public void testFormInteractions() {
// Navigate to form
MobileElement formTab = driver.findElementByAccessibilityId("form-tab");
formTab.click();
// Text input
MobileElement nameField = driver.findElementByAccessibilityId("name-field");
nameField.sendKeys("John Doe");
assertEquals("John Doe", nameField.getText());
// Platform-specific dropdown handling
MobileElement countryPicker = driver.findElementByAccessibilityId("country-picker");
countryPicker.click();
if (driver instanceof IOSDriver) {
// iOS Picker
MobileElement usOption = driver.findElementByAccessibilityId("United States");
usOption.click();
MobileElement doneButton = driver.findElementByAccessibilityId("Done");
doneButton.click();
} else {
// Android Spinner
MobileElement usOption = driver.findElementByAndroidUIAutomator(
"new UiSelector().text(\"United States\")"
);
usOption.click();
}
// Toggle switch
MobileElement notificationSwitch = driver.findElementByAccessibilityId("notification-switch");
notificationSwitch.click();
assertEquals("1", notificationSwitch.getAttribute("value"));
// Submit form
MobileElement submitButton = driver.findElementByAccessibilityId("submit-form");
submitButton.click();
// Verify success
MobileElement successMessage = driver.findElementByAccessibilityId("success-message");
WebDriverWait wait = new WebDriverWait(driver, 5);
wait.until(ExpectedConditions.visibilityOf(successMessage));
assertTrue(successMessage.getText().contains("Form submitted successfully"));
}
@Test
public void testDeviceSpecificFeatures() {
// Test orientation change
driver.rotate(ScreenOrientation.LANDSCAPE);
// Verify layout adaptation
MobileElement headerTitle = driver.findElementByAccessibilityId("header-title");
int landscapeHeight = headerTitle.getSize().getHeight();
assertTrue("Header should be compact in landscape", landscapeHeight < 100);
driver.rotate(ScreenOrientation.PORTRAIT);
// Test app backgrounding
driver.runAppInBackground(Duration.ofSeconds(3));
// Verify app resumed correctly
if (driver instanceof AndroidDriver) {
String currentActivity = ((AndroidDriver) driver).currentActivity();
assertTrue(currentActivity.contains("MainActivity"));
}
}
@Test
public void testScrollingAndLists() {
// Navigate to list screen
MobileElement listTab = driver.findElementByAccessibilityId("list-tab");
listTab.click();
// Wait for list
MobileElement itemList = driver.findElementByAccessibilityId("item-list");
WebDriverWait wait = new WebDriverWait(driver, 10);
wait.until(ExpectedConditions.visibilityOf(itemList));
// Scroll to find specific item
MobileElement targetItem = null;
int scrollAttempts = 0;
int maxScrolls = 5;
TouchAction touchAction = new TouchAction(driver);
while (scrollAttempts < maxScrolls) {
try {
targetItem = driver.findElementByAccessibilityId("item-special");
if (targetItem.isDisplayed()) {
break;
}
} catch (Exception e) {
// Element not found, continue scrolling
}
// Scroll down
Dimension size = driver.manage().window().getSize();
int startY = (int) (size.height * 0.8);
int endY = (int) (size.height * 0.2);
int centerX = size.width / 2;
touchAction
.press(PointOption.point(centerX, startY))
.waitAction(WaitOptions.waitOptions(Duration.ofSeconds(1)))
.moveTo(PointOption.point(centerX, endY))
.release()
.perform();
scrollAttempts++;
}
if (targetItem != null && targetItem.isDisplayed()) {
targetItem.click();
// Verify item details screen
MobileElement itemDetails = driver.findElementByAccessibilityId("item-details");
wait.until(ExpectedConditions.visibilityOf(itemDetails));
assertTrue(itemDetails.isDisplayed());
}
}
@Test
public void testPerformanceMonitoring() {
// Start performance monitoring
driver.startPerformanceLogging();
// Perform app operations
MobileElement heavyOperationButton = driver.findElementByAccessibilityId("heavy-operation");
long startTime = System.currentTimeMillis();
heavyOperationButton.click();
// Wait for operation completion
MobileElement completionIndicator = driver.findElementByAccessibilityId("operation-complete");
WebDriverWait wait = new WebDriverWait(driver, 30);
wait.until(ExpectedConditions.visibilityOf(completionIndicator));
long endTime = System.currentTimeMillis();
long operationTime = endTime - startTime;
// Assert performance criteria
assertTrue("Operation should complete within 10 seconds", operationTime < 10000);
// Get performance logs (Android)
if (driver instanceof AndroidDriver) {
LogEntries logs = driver.manage().logs().get("performance");
assertFalse("Performance logs should not be empty", logs.getAll().isEmpty());
}
}
}
# Appium with Python and pytest
from appium import webdriver
from appium.webdriver.common.touch_action import TouchAction
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import pytest
import time
class TestMobileApp:
@pytest.fixture(scope="class", autouse=True)
def setup_driver(self):
# iOS capabilities
ios_caps = {
'platformName': 'iOS',
'platformVersion': '16.0',
'deviceName': 'iPhone 14',
'app': '/path/to/your/app.ipa',
'automationName': 'XCUITest',
'noReset': False,
'fullReset': True,
'newCommandTimeout': 300
}
# Android capabilities
android_caps = {
'platformName': 'Android',
'platformVersion': '13.0',
'deviceName': 'Pixel_7_API_33',
'app': '/path/to/your/app.apk',
'automationName': 'UiAutomator2',
'appPackage': 'com.example.app',
'appActivity': '.MainActivity',
'noReset': False,
'fullReset': True,
'newCommandTimeout': 300
}
# Use Android capabilities by default
capabilities = android_caps
self.driver = webdriver.Remote(
command_executor='http://localhost:4723/wd/hub',
desired_capabilities=capabilities
)
yield
if self.driver:
self.driver.quit()
def test_successful_login(self):
"""Test successful user login"""
# Wait for and interact with login elements
wait = WebDriverWait(self.driver, 10)
username_field = wait.until(
EC.presence_of_element_located(("accessibility id", "username-input"))
)
password_field = self.driver.find_element("accessibility id", "password-input")
login_button = self.driver.find_element("accessibility id", "login-button")
username_field.send_keys("testuser@example.com")
password_field.send_keys("password123")
login_button.click()
# Verify successful login
home_screen = wait.until(
EC.visibility_of_element_located(("accessibility id", "home-screen"))
)
assert home_screen.is_displayed()
def test_touch_gestures(self):
"""Test various touch gestures"""
touch_action = TouchAction(self.driver)
# Tap gesture
tap_button = self.driver.find_element("accessibility id", "tap-button")
touch_action.tap(tap_button).perform()
# Swipe gesture - left swipe on carousel
carousel = self.driver.find_element("accessibility id", "image-carousel")
carousel_center = carousel.location_in_view
touch_action.press(x=carousel_center['x'] + 100, y=carousel_center['y']) \
.wait(1000) \
.move_to(x=carousel_center['x'] - 100, y=carousel_center['y']) \
.release() \
.perform()
# Long press gesture
long_press_element = self.driver.find_element("accessibility id", "long-press-item")
touch_action.long_press(long_press_element, duration=2000).release().perform()
# Verify context menu appeared
context_menu = self.driver.find_element("accessibility id", "context-menu")
assert context_menu.is_displayed()
def test_form_interactions(self):
"""Test form element interactions"""
# Navigate to form
form_tab = self.driver.find_element("accessibility id", "form-tab")
form_tab.click()
# Text input
name_field = self.driver.find_element("accessibility id", "name-field")
name_field.send_keys("John Doe")
assert name_field.get_attribute("value") == "John Doe"
# Platform-specific dropdown handling
country_picker = self.driver.find_element("accessibility id", "country-picker")
country_picker.click()
if self.driver.capabilities['platformName'] == 'iOS':
# iOS Picker
us_option = self.driver.find_element("accessibility id", "United States")
us_option.click()
done_button = self.driver.find_element("accessibility id", "Done")
done_button.click()
else:
# Android Spinner
us_option = self.driver.find_element(
"android uiautomator",
'new UiSelector().text("United States")'
)
us_option.click()
# Toggle switch
notification_switch = self.driver.find_element("accessibility id", "notification-switch")
notification_switch.click()
assert notification_switch.get_attribute("value") == "1"
# Submit form
submit_button = self.driver.find_element("accessibility id", "submit-form")
submit_button.click()
# Verify success
wait = WebDriverWait(self.driver, 5)
success_message = wait.until(
EC.visibility_of_element_located(("accessibility id", "success-message"))
)
assert "Form submitted successfully" in success_message.text
def test_device_specific_features(self):
"""Test device-specific functionality"""
# Test orientation change
self.driver.orientation = "LANDSCAPE"
# Verify layout adaptation
header_title = self.driver.find_element("accessibility id", "header-title")
landscape_height = header_title.size['height']
assert landscape_height < 100, "Header should be compact in landscape"
self.driver.orientation = "PORTRAIT"
# Test app backgrounding
self.driver.background_app(3) # 3 seconds in background
# Verify app resumed correctly
if self.driver.capabilities['platformName'] == 'Android':
current_activity = self.driver.current_activity
assert "MainActivity" in current_activity
def test_scrolling_and_lists(self):
"""Test scrolling and list interactions"""
# Navigate to list screen
list_tab = self.driver.find_element("accessibility id", "list-tab")
list_tab.click()
# Wait for list
wait = WebDriverWait(self.driver, 10)
item_list = wait.until(
EC.visibility_of_element_located(("accessibility id", "item-list"))
)
# Scroll to find specific item
target_item = None
scroll_attempts = 0
max_scrolls = 5
touch_action = TouchAction(self.driver)
while scroll_attempts < max_scrolls:
try:
target_item = self.driver.find_element("accessibility id", "item-special")
if target_item.is_displayed():
break
except:
# Element not found, continue scrolling
pass
# Scroll down
size = self.driver.get_window_size()
start_y = int(size['height'] * 0.8)
end_y = int(size['height'] * 0.2)
center_x = size['width'] // 2
touch_action.press(x=center_x, y=start_y) \
.wait(1000) \
.move_to(x=center_x, y=end_y) \
.release() \
.perform()
scroll_attempts += 1
if target_item and target_item.is_displayed():
target_item.click()
# Verify item details screen
item_details = wait.until(
EC.visibility_of_element_located(("accessibility id", "item-details"))
)
assert item_details.is_displayed()
def test_performance_monitoring(self):
"""Test app performance during operations"""
# Perform operation and measure time
heavy_operation_button = self.driver.find_element("accessibility id", "heavy-operation")
start_time = time.time()
heavy_operation_button.click()
# Wait for operation completion
wait = WebDriverWait(self.driver, 30)
completion_indicator = wait.until(
EC.visibility_of_element_located(("accessibility id", "operation-complete"))
)
end_time = time.time()
operation_time = (end_time - start_time) * 1000 # Convert to milliseconds
# Assert performance criteria
assert operation_time < 10000, f"Operation took {operation_time}ms, should be under 10s"
assert completion_indicator.is_displayed()
def test_network_conditions(self):
"""Test app behavior under different network conditions"""
# Set network condition to slow 3G
self.driver.set_network_connection(2) # 2 = Data only
# Perform network-dependent operation
refresh_button = self.driver.find_element("accessibility id", "refresh-data")
refresh_button.click()
# Wait for loading indicator
loading_indicator = self.driver.find_element("accessibility id", "loading-spinner")
assert loading_indicator.is_displayed()
# Wait for data to load (should take longer on slow connection)
wait = WebDriverWait(self.driver, 20)
data_content = wait.until(
EC.visibility_of_element_located(("accessibility id", "data-content"))
)
assert data_content.is_displayed()
# Reset network to normal
self.driver.set_network_connection(6) # 6 = All network on
def test_app_permissions(self):
"""Test app permissions handling"""
# Trigger camera permission request
camera_button = self.driver.find_element("accessibility id", "camera-button")
camera_button.click()
if self.driver.capabilities['platformName'] == 'Android':
try:
# Handle Android permission dialog
allow_button = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((By.ID, "com.android.permissioncontroller:id/permission_allow_button"))
)
allow_button.click()
except:
# Permission already granted or dialog didn't appear
pass
else:
try:
# Handle iOS permission alert
allow_button = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable(("accessibility id", "Allow"))
)
allow_button.click()
except:
# Permission already granted or alert didn't appear
pass
# Verify camera functionality is accessible
camera_view = self.driver.find_element("accessibility id", "camera-view")
assert camera_view.is_displayed()
Cross-Platform Testing Strategies
Effective strategies for testing applications across both iOS and Android platforms:
// Cross-platform testing strategy with WebdriverIO
const { remote } = require('webdriverio');
class CrossPlatformTestRunner {
constructor() {
this.platforms = [
{
name: 'iOS',
capabilities: {
platformName: 'iOS',
platformVersion: '16.0',
deviceName: 'iPhone 14',
app: '/path/to/app.ipa',
automationName: 'XCUITest'
}
},
{
name: 'Android',
capabilities: {
platformName: 'Android',
platformVersion: '13.0',
deviceName: 'Pixel 7',
app: '/path/to/app.apk',
automationName: 'UiAutomator2',
appPackage: 'com.example.app',
appActivity: '.MainActivity'
}
}
];
}
async runCrossPlatformTest(testFunction) {
const results = {};
for (const platform of this.platforms) {
console.log(`Running test on ${platform.name}`);
const driver = await remote({
protocol: 'http',
hostname: 'localhost',
port: 4723,
path: '/wd/hub/',
capabilities: platform.capabilities
});
try {
const result = await testFunction(driver, platform.name);
results[platform.name] = { success: true, result };
} catch (error) {
results[platform.name] = { success: false, error: error.message };
} finally {
await driver.deleteSession();
}
}
return results;
}
// Platform-specific element selectors
getElementSelector(elementName, platform) {
const selectors = {
'login-button': {
iOS: '~login-btn',
Android: '~login-button'
},
'username-field': {
iOS: '~username-input',
Android: 'android=new UiSelector().resourceId("username")'
},
'dropdown-option': {
iOS: (text) => `~${text}`,
Android: (text) => `android=new UiSelector().text("${text}")`
}
};
return selectors[elementName] ? selectors[elementName][platform] : `~${elementName}`;
}
// Platform-specific actions
async performPlatformAction(driver, platform, actionType, params) {
switch (actionType) {
case 'selectDropdownOption':
if (platform === 'iOS') {
// iOS Picker approach
const picker = await driver.$(params.pickerSelector);
await picker.click();
const option = await driver.$(params.optionSelector);
await option.click();
const done = await driver.$('~Done');
await done.click();
} else {
// Android Spinner approach
const spinner = await driver.$(params.pickerSelector);
await spinner.click();
const option = await driver.$(params.optionSelector);
await option.click();
}
break;
case 'handlePermissionDialog':
if (platform === 'iOS') {
try {
const allowButton = await driver.$('~Allow');
if (await allowButton.isDisplayed()) {
await allowButton.click();
}
} catch (e) {
// Permission dialog not shown or already handled
}
} else {
try {
const allowButton = await driver.$('id:com.android.permissioncontroller:id/permission_allow_button');
if (await allowButton.isDisplayed()) {
await allowButton.click();
}
} catch (e) {
// Permission dialog not shown or already handled
}
}
break;
case 'scrollToElement':
// Different scrolling strategies per platform
if (platform === 'iOS') {
// Use iOS-specific scrolling
await driver.execute('mobile: scroll', {
direction: 'down',
element: params.scrollContainer
});
} else {
// Use Android UiScrollable
const element = await driver.$(`android=new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("${params.targetText}"))`);
return element;
}
break;
}
}
}
// Example cross-platform test
describe('Cross-Platform App Tests', () => {
const testRunner = new CrossPlatformTestRunner();
it('should login successfully on both platforms', async () => {
const loginTest = async (driver, platform) => {
// Use platform-specific selectors
const usernameSelector = testRunner.getElementSelector('username-field', platform);
const loginButtonSelector = testRunner.getElementSelector('login-button', platform);
const usernameField = await driver.$(usernameSelector);
const passwordField = await driver.$('~password-input');
const loginButton = await driver.$(loginButtonSelector);
await usernameField.setValue('test@example.com');
await passwordField.setValue('password123');
await loginButton.click();
// Verify login success
const homeScreen = await driver.$('~home-screen');
await homeScreen.waitForDisplayed({ timeout: 10000 });
return { loginSuccessful: await homeScreen.isDisplayed() };
};
const results = await testRunner.runCrossPlatformTest(loginTest);
// Verify both platforms succeeded
expect(results.iOS.success).toBe(true);
expect(results.Android.success).toBe(true);
expect(results.iOS.result.loginSuccessful).toBe(true);
expect(results.Android.result.loginSuccessful).toBe(true);
});
it('should handle dropdowns correctly on both platforms', async () => {
const dropdownTest = async (driver, platform) => {
const formTab = await driver.$('~form-tab');
await formTab.click();
// Handle platform-specific dropdown interaction
await testRunner.performPlatformAction(driver, platform, 'selectDropdownOption', {
pickerSelector: '~country-picker',
optionSelector: platform === 'iOS' ? '~United States' : 'android=new UiSelector().text("United States")'
});
// Verify selection
const selectedValue = await driver.$('~selected-country');
const text = await selectedValue.getText();
return { selectedCountry: text };
};
const results = await testRunner.runCrossPlatformTest(dropdownTest);
expect(results.iOS.success).toBe(true);
expect(results.Android.success).toBe(true);
expect(results.iOS.result.selectedCountry).toContain('United States');
expect(results.Android.result.selectedCountry).toContain('United States');
});
});
// Cross-platform testing with Java
import org.testng.annotations.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class CrossPlatformMobileTest {
private Map> drivers;
private Map platformCapabilities;
@BeforeClass
public void setupPlatforms() {
drivers = new HashMap<>();
platformCapabilities = new HashMap<>();
// iOS capabilities
DesiredCapabilities iosCapabilities = new DesiredCapabilities();
iosCapabilities.setCapability("platformName", "iOS");
iosCapabilities.setCapability("platformVersion", "16.0");
iosCapabilities.setCapability("deviceName", "iPhone 14");
iosCapabilities.setCapability("app", "/path/to/app.ipa");
iosCapabilities.setCapability("automationName", "XCUITest");
platformCapabilities.put("iOS", iosCapabilities);
// Android capabilities
DesiredCapabilities androidCapabilities = new DesiredCapabilities();
androidCapabilities.setCapability("platformName", "Android");
androidCapabilities.setCapability("platformVersion", "13.0");
androidCapabilities.setCapability("deviceName", "Pixel_7_API_33");
androidCapabilities.setCapability("app", "/path/to/app.apk");
androidCapabilities.setCapability("automationName", "UiAutomator2");
androidCapabilities.setCapability("appPackage", "com.example.app");
androidCapabilities.setCapability("appActivity", ".MainActivity");
platformCapabilities.put("Android", androidCapabilities);
}
@BeforeMethod
public void setupDrivers() throws Exception {
// Initialize both platform drivers
for (String platform : platformCapabilities.keySet()) {
AppiumDriver driver;
if (platform.equals("iOS")) {
driver = new IOSDriver<>(
new URL("http://localhost:4723/wd/hub"),
platformCapabilities.get(platform)
);
} else {
driver = new AndroidDriver<>(
new URL("http://localhost:4723/wd/hub"),
platformCapabilities.get(platform)
);
}
drivers.put(platform, driver);
}
}
@AfterMethod
public void tearDownDrivers() {
drivers.values().forEach(driver -> {
if (driver != null) {
driver.quit();
}
});
drivers.clear();
}
@Test
public void testCrossPlatformLogin() {
Map> results = new HashMap<>();
// Run login test on both platforms simultaneously
for (Map.Entry> entry : drivers.entrySet()) {
String platform = entry.getKey();
AppiumDriver driver = entry.getValue();
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
return performLoginTest(driver, platform);
});
results.put(platform, future);
}
// Wait for both tests to complete and verify results
results.forEach((platform, future) -> {
try {
Boolean loginSuccess = future.get();
assertTrue("Login should succeed on " + platform, loginSuccess);
} catch (Exception e) {
fail("Login test failed on " + platform + ": " + e.getMessage());
}
});
}
private Boolean performLoginTest(AppiumDriver driver, String platform) {
try {
// Platform-specific element selection
MobileElement usernameField = getPlatformElement(driver, platform, "username-field");
MobileElement passwordField = driver.findElementByAccessibilityId("password-input");
MobileElement loginButton = getPlatformElement(driver, platform, "login-button");
usernameField.sendKeys("test@example.com");
passwordField.sendKeys("password123");
loginButton.click();
// Verify login success
WebDriverWait wait = new WebDriverWait(driver, 10);
MobileElement homeScreen = wait.until(
ExpectedConditions.visibilityOfElementLocated(
MobileBy.AccessibilityId("home-screen")
)
);
return homeScreen.isDisplayed();
} catch (Exception e) {
System.err.println("Login test failed on " + platform + ": " + e.getMessage());
return false;
}
}
@Test
public void testCrossPlatformFormInteraction() {
for (Map.Entry> entry : drivers.entrySet()) {
String platform = entry.getKey();
AppiumDriver driver = entry.getValue();
// Navigate to form
MobileElement formTab = driver.findElementByAccessibilityId("form-tab");
formTab.click();
// Platform-specific dropdown handling
handleDropdownSelection(driver, platform, "United States");
// Verify selection
MobileElement selectedValue = driver.findElementByAccessibilityId("selected-country");
String text = selectedValue.getText();
assertTrue("Country should be selected on " + platform,
text.contains("United States"));
}
}
private void handleDropdownSelection(AppiumDriver driver, String platform, String option) {
MobileElement dropdown = driver.findElementByAccessibilityId("country-picker");
dropdown.click();
if (platform.equals("iOS")) {
// iOS Picker handling
MobileElement pickerOption = driver.findElementByAccessibilityId(option);
pickerOption.click();
MobileElement doneButton = driver.findElementByAccessibilityId("Done");
doneButton.click();
} else {
// Android Spinner handling
MobileElement spinnerOption = ((AndroidDriver) driver).findElementByAndroidUIAutomator(
"new UiSelector().text(\"" + option + "\")"
);
spinnerOption.click();
}
}
private MobileElement getPlatformElement(AppiumDriver driver, String platform, String elementType) {
switch (elementType) {
case "username-field":
if (platform.equals("iOS")) {
return driver.findElementByAccessibilityId("username-input");
} else {
return ((AndroidDriver) driver).findElementByAndroidUIAutomator(
"new UiSelector().resourceId(\"username\")"
);
}
case "login-button":
if (platform.equals("iOS")) {
return driver.findElementByAccessibilityId("login-btn");
} else {
return driver.findElementByAccessibilityId("login-button");
}
default:
return driver.findElementByAccessibilityId(elementType);
}
}
@Test
public void testPerformanceComparison() {
Map performanceResults = new HashMap<>();
for (Map.Entry> entry : drivers.entrySet()) {
String platform = entry.getKey();
AppiumDriver driver = entry.getValue();
// Measure app launch time
long startTime = System.currentTimeMillis();
// Trigger heavy operation
MobileElement heavyOperationButton = driver.findElementByAccessibilityId("heavy-operation");
heavyOperationButton.click();
// Wait for completion
WebDriverWait wait = new WebDriverWait(driver, 30);
wait.until(ExpectedConditions.visibilityOfElementLocated(
MobileBy.AccessibilityId("operation-complete")
));
long endTime = System.currentTimeMillis();
long operationTime = endTime - startTime;
performanceResults.put(platform, operationTime);
// Assert reasonable performance
assertTrue("Operation should complete within 15 seconds on " + platform,
operationTime < 15000);
}
// Compare performance between platforms
System.out.println("Performance Results:");
performanceResults.forEach((platform, time) -> {
System.out.println(platform + ": " + time + "ms");
});
}
@Test(dataProvider = "deviceMatrix")
public void testDeviceMatrix(String platform, String deviceName, String platformVersion) throws Exception {
// Test on specific device configurations
DesiredCapabilities caps = new DesiredCapabilities(platformCapabilities.get(platform));
caps.setCapability("deviceName", deviceName);
caps.setCapability("platformVersion", platformVersion);
AppiumDriver driver;
if (platform.equals("iOS")) {
driver = new IOSDriver<>(new URL("http://localhost:4723/wd/hub"), caps);
} else {
driver = new AndroidDriver<>(new URL("http://localhost:4723/wd/hub"), caps);
}
try {
// Run basic functionality test
Boolean loginSuccess = performLoginTest(driver, platform);
assertTrue("Login should work on " + platform + " " + deviceName + " " + platformVersion,
loginSuccess);
} finally {
driver.quit();
}
}
@DataProvider(name = "deviceMatrix")
public Object[][] deviceMatrix() {
return new Object[][] {
{"iOS", "iPhone 14", "16.0"},
{"iOS", "iPhone 13", "15.7"},
{"iOS", "iPad Pro", "16.0"},
{"Android", "Pixel 7", "13.0"},
{"Android", "Samsung Galaxy S23", "13.0"},
{"Android", "OnePlus 11", "13.0"}
};
}
}
# Cross-platform mobile testing with Python
import pytest
import asyncio
from concurrent.futures import ThreadPoolExecutor
from appium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class CrossPlatformMobileTest:
def setup_class(self):
"""Setup platform configurations"""
self.platforms = {
'iOS': {
'platformName': 'iOS',
'platformVersion': '16.0',
'deviceName': 'iPhone 14',
'app': '/path/to/app.ipa',
'automationName': 'XCUITest',
'noReset': False,
'fullReset': True
},
'Android': {
'platformName': 'Android',
'platformVersion': '13.0',
'deviceName': 'Pixel_7_API_33',
'app': '/path/to/app.apk',
'automationName': 'UiAutomator2',
'appPackage': 'com.example.app',
'appActivity': '.MainActivity',
'noReset': False,
'fullReset': True
}
}
def create_driver(self, platform):
"""Create driver for specific platform"""
capabilities = self.platforms[platform]
return webdriver.Remote(
command_executor='http://localhost:4723/wd/hub',
desired_capabilities=capabilities
)
def get_platform_element(self, driver, platform, element_type):
"""Get platform-specific element selectors"""
selectors = {
'username-field': {
'iOS': ("accessibility id", "username-input"),
'Android': ("android uiautomator", 'new UiSelector().resourceId("username")')
},
'login-button': {
'iOS': ("accessibility id", "login-btn"),
'Android': ("accessibility id", "login-button")
},
'dropdown-option': {
'iOS': lambda text: ("accessibility id", text),
'Android': lambda text: ("android uiautomator", f'new UiSelector().text("{text}")')
}
}
if element_type in selectors:
selector = selectors[element_type][platform]
if callable(selector):
return selector
return driver.find_element(*selector)
else:
return driver.find_element("accessibility id", element_type)
def perform_platform_action(self, driver, platform, action_type, **kwargs):
"""Perform platform-specific actions"""
if action_type == 'select_dropdown_option':
dropdown = driver.find_element("accessibility id", kwargs['dropdown_id'])
dropdown.click()
if platform == 'iOS':
# iOS Picker approach
option = driver.find_element("accessibility id", kwargs['option_text'])
option.click()
done_button = driver.find_element("accessibility id", "Done")
done_button.click()
else:
# Android Spinner approach
option = driver.find_element(
"android uiautomator",
f'new UiSelector().text("{kwargs["option_text"]}")'
)
option.click()
elif action_type == 'handle_permission_dialog':
if platform == 'iOS':
try:
allow_button = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable(("accessibility id", "Allow"))
)
allow_button.click()
except:
pass # No permission dialog or already handled
else:
try:
allow_button = WebDriverWait(driver, 5).until(
EC.element_to_be_clickable(("id", "com.android.permissioncontroller:id/permission_allow_button"))
)
allow_button.click()
except:
pass # No permission dialog or already handled
def run_on_all_platforms(self, test_function):
"""Run test function on all platforms"""
results = {}
for platform in self.platforms.keys():
driver = None
try:
driver = self.create_driver(platform)
result = test_function(driver, platform)
results[platform] = {'success': True, 'result': result}
except Exception as e:
results[platform] = {'success': False, 'error': str(e)}
finally:
if driver:
driver.quit()
return results
def test_cross_platform_login(self):
"""Test login functionality across platforms"""
def login_test(driver, platform):
# Get platform-specific elements
username_field = self.get_platform_element(driver, platform, 'username-field')
password_field = driver.find_element("accessibility id", "password-input")
login_button = self.get_platform_element(driver, platform, 'login-button')
# Perform login
username_field.send_keys("test@example.com")
password_field.send_keys("password123")
login_button.click()
# Verify success
wait = WebDriverWait(driver, 10)
home_screen = wait.until(
EC.visibility_of_element_located(("accessibility id", "home-screen"))
)
return {'login_successful': home_screen.is_displayed()}
results = self.run_on_all_platforms(login_test)
# Verify both platforms succeeded
for platform, result in results.items():
assert result['success'], f"Login test failed on {platform}: {result.get('error', 'Unknown error')}"
assert result['result']['login_successful'], f"Login not successful on {platform}"
def test_cross_platform_form_interaction(self):
"""Test form interactions across platforms"""
def form_test(driver, platform):
# Navigate to form
form_tab = driver.find_element("accessibility id", "form-tab")
form_tab.click()
# Handle dropdown selection
self.perform_platform_action(
driver, platform, 'select_dropdown_option',
dropdown_id='country-picker',
option_text='United States'
)
# Verify selection
selected_value = driver.find_element("accessibility id", "selected-country")
return {'selected_country': selected_value.text}
results = self.run_on_all_platforms(form_test)
# Verify results
for platform, result in results.items():
assert result['success'], f"Form test failed on {platform}"
assert 'United States' in result['result']['selected_country'], f"Country not selected on {platform}"
def test_parallel_execution(self):
"""Test parallel execution across platforms"""
def performance_test(driver, platform):
import time
start_time = time.time()
# Perform heavy operation
heavy_operation_button = driver.find_element("accessibility id", "heavy-operation")
heavy_operation_button.click()
# Wait for completion
wait = WebDriverWait(driver, 30)
completion_indicator = wait.until(
EC.visibility_of_element_located(("accessibility id", "operation-complete"))
)
end_time = time.time()
operation_time = (end_time - start_time) * 1000
return {
'operation_time_ms': operation_time,
'completed': completion_indicator.is_displayed()
}
# Run tests in parallel using ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=len(self.platforms)) as executor:
futures = {}
for platform in self.platforms.keys():
future = executor.submit(lambda p: self.run_on_all_platforms(lambda d, pl: performance_test(d, pl) if pl == p else None), platform)
futures[platform] = future
# Collect results
parallel_results = {}
for platform, future in futures.items():
try:
result = future.result(timeout=60) # 60 second timeout
if platform in result and result[platform]['success']:
parallel_results[platform] = result[platform]['result']
except Exception as e:
parallel_results[platform] = {'error': str(e)}
# Verify performance results
for platform, result in parallel_results.items():
if 'error' not in result:
assert result['completed'], f"Operation did not complete on {platform}"
assert result['operation_time_ms'] < 15000, f"Operation too slow on {platform}: {result['operation_time_ms']}ms"
@pytest.mark.parametrize("platform,device_name,platform_version", [
("iOS", "iPhone 14", "16.0"),
("iOS", "iPhone 13", "15.7"),
("iOS", "iPad Pro", "16.0"),
("Android", "Pixel 7", "13.0"),
("Android", "Samsung Galaxy S23", "13.0"),
("Android", "OnePlus 11", "13.0")
])
def test_device_matrix(self, platform, device_name, platform_version):
"""Test on specific device configurations"""
capabilities = self.platforms[platform].copy()
capabilities['deviceName'] = device_name
capabilities['platformVersion'] = platform_version
driver = webdriver.Remote(
command_executor='http://localhost:4723/wd/hub',
desired_capabilities=capabilities
)
try:
# Basic functionality test
username_field = self.get_platform_element(driver, platform, 'username-field')
password_field = driver.find_element("accessibility id", "password-input")
login_button = self.get_platform_element(driver, platform, 'login-button')
username_field.send_keys("test@example.com")
password_field.send_keys("password123")
login_button.click()
# Verify login success
wait = WebDriverWait(driver, 10)
home_screen = wait.until(
EC.visibility_of_element_located(("accessibility id", "home-screen"))
)
assert home_screen.is_displayed(), f"Login failed on {platform} {device_name} {platform_version}"
finally:
driver.quit()
def test_network_conditions_cross_platform(self):
"""Test app behavior under different network conditions across platforms"""
def network_test(driver, platform):
# Set network to slow connection
if platform == 'Android':
driver.set_network_connection(2) # Data only, slow
# Perform network operation
refresh_button = driver.find_element("accessibility id", "refresh-data")
refresh_button.click()
# Wait for data load with extended timeout for slow network
wait = WebDriverWait(driver, 20)
data_content = wait.until(
EC.visibility_of_element_located(("accessibility id", "data-content"))
)
# Reset network
if platform == 'Android':
driver.set_network_connection(6) # All network on
return {'data_loaded': data_content.is_displayed()}
results = self.run_on_all_platforms(network_test)
# Verify network handling
for platform, result in results.items():
assert result['success'], f"Network test failed on {platform}"
assert result['result']['data_loaded'], f"Data not loaded on {platform}"
Best Practices for Mobile Test Automation
- Device Strategy: Test on real devices when possible, use simulators/emulators for broader coverage
- Platform-Specific Testing: Account for iOS and Android UI/UX differences in test design
- Network Conditions: Test app behavior under various network conditions (3G, 4G, WiFi, offline)
- Device Fragmentation: Test on multiple device sizes, resolutions, and OS versions
- Battery and Performance: Monitor app performance impact on battery life and device resources
- Gestures and Touch: Thoroughly test touch interactions, swipes, pinch-to-zoom, and multi-touch
- Permissions: Test app permission requests and handling of granted/denied permissions
- Background/Foreground: Verify app state management when backgrounded or interrupted
- Accessibility: Test with accessibility features enabled (VoiceOver, TalkBack)
- Cloud Testing: Utilize device clouds for broader device coverage and parallel execution