← Back to Learning Modules

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