pCloudy Real Device Testing: Appium and Native Automation

pCloudy Real Device Testing: Appium and Native Automation

Running Appium tests against pCloudy's real device cloud is fundamentally the same as running against a local Appium server — you're still sending WebDriver commands — but the capabilities configuration, authentication, and parallel execution model have pCloudy-specific details that take time to figure out from documentation alone. This guide covers the full setup, common capability configurations for iOS and Android, parallel execution, and Espresso/XCUITest native testing.

How pCloudy's Appium Grid Works

pCloudy operates an Appium hub at:

https://device.pcloudy.com/appiumcloud/wd/hub

When you create a RemoteWebDriver session pointing at this URL with your capabilities, pCloudy:

  1. Authenticates your credentials from the pCloudy_Username and pCloudy_ApiKey capabilities
  2. Finds an available device matching your requested specifications
  3. Installs your specified app on that device
  4. Starts an Appium server session
  5. Returns a session ID and proxies all subsequent WebDriver commands to the physical device

The device stays allocated to your session until you call driver.quit() or the pCloudy_DurationInMinutes expires. You pay for the full booked duration regardless of when your tests finish, so calling quit() explicitly at the end of your test saves device minutes.

Android Capabilities Deep Dive

Minimal Working Configuration

UiAutomator2Options options = new UiAutomator2Options();

// Auth
options.setCapability("pCloudy_Username", System.getenv("PCLOUDY_USERNAME"));
options.setCapability("pCloudy_ApiKey", System.getenv("PCLOUDY_API_KEY"));

// Device targeting
options.setCapability("pCloudy_DeviceFullName", "Samsung Galaxy S22_12.0_");
options.setCapability("pCloudy_DeviceVersion", "12.0");
options.setCapability("pCloudy_DurationInMinutes", "15");

// App
options.setCapability("pCloudy_ApplicationName",
    "Appium_Test/user@company.com/1700000000_app.apk");
options.setCapability("appPackage", "com.company.app");
options.setCapability("appActivity", "com.company.app.MainActivity");
options.setCapability("automationName", "UiAutomator2");
options.setCapability("newCommandTimeout", 120);

AndroidDriver driver = new AndroidDriver(
    new URL("https://device.pcloudy.com/appiumcloud/wd/hub"),
    options
);

Device Targeting Options

Exact device:

options.setCapability("pCloudy_DeviceFullName", "Samsung Galaxy S22_12.0_");

Any available device matching OS version:

options.setCapability("pCloudy_DeviceVersion", "12.0");
options.setCapability("pCloudy_WildNet", "true");

With pCloudy_WildNet=true, pCloudy picks any available device at the specified OS version. Useful for CI where you want to avoid queue waits on specific devices.

Query available devices first:

curl "https://device.pcloudy.com/api/devices?username=user@company.com&access_key=TOKEN&available_now=true&os=android&duration=15" \
  | python3 -m json.tool

The response includes full_name for each device — use exactly this string in pCloudy_DeviceFullName.

Important Android Capabilities

noReset: When true, doesn't reinstall the app between sessions. Faster session startup but you inherit whatever state the previous session left. Use false (default) for clean test isolation.

options.setCapability("noReset", false);
options.setCapability("fullReset", false); // fullReset=true wipes device data

autoGrantPermissions: Auto-approves all permission dialogs during app launch. Without this, your test will stall waiting for an alert that your automation isn't handling.

options.setCapability("autoGrantPermissions", true);

newCommandTimeout: Seconds Appium waits between commands before killing the session. Set this higher than your longest expected test pause.

options.setCapability("newCommandTimeout", 300);

Screenshot on failure:

options.setCapability("pCloudy_EnablePerformanceData", "true");

iOS Capabilities Deep Dive

Minimal Working Configuration

XCUITestOptions iosOptions = new XCUITestOptions();

// Auth
iosOptions.setCapability("pCloudy_Username", System.getenv("PCLOUDY_USERNAME"));
iosOptions.setCapability("pCloudy_ApiKey", System.getenv("PCLOUDY_API_KEY"));

// Device
iosOptions.setCapability("pCloudy_DeviceFullName", "Apple iPhone 14_16.2_");
iosOptions.setCapability("pCloudy_DeviceVersion", "16.2");
iosOptions.setCapability("pCloudy_DurationInMinutes", "15");

// App
iosOptions.setCapability("pCloudy_ApplicationName",
    "Appium_Test/user@company.com/1700000000_MyApp.ipa");
iosOptions.setCapability("bundleId", "com.company.app");
iosOptions.setCapability("automationName", "XCUITest");
iosOptions.setCapability("newCommandTimeout", 120);

IOSDriver iosDriver = new IOSDriver(
    new URL("https://device.pcloudy.com/appiumcloud/wd/hub"),
    iosOptions
);

iOS Signing Requirements

The most common iOS setup problem is code signing. Your IPA must be signed in a way that allows it to run on pCloudy's physical devices.

Options:

  1. Enterprise Distribution Certificate: Sign with an Apple Enterprise account certificate. No provisioning profile restrictions — any device can run it.
  2. Ad-Hoc Distribution: Sign with a distribution certificate and a provisioning profile that explicitly includes pCloudy's device UDIDs. Request the UDID list from pCloudy support.
  3. Development Certificate with pCloudy UDIDs: Works for internal apps during development phase. Requires the pCloudy device UDIDs in your provisioning profile.

To resign an IPA without rebuilding from source:

# Install ios-deploy and fastlane
gem install fastlane

# Resign with enterprise cert
fastlane run resign ipa:"./MyApp.ipa" \
  signing_identity:"iPhone Distribution: Company Name (TEAM_ID)" \
  provisioning_profile:"./Enterprise.mobileprovision"

iOS-Specific Capabilities

// Prevent app reinstallation for faster subsequent runs
iosOptions.setCapability("noReset", true);

// Handle system alerts automatically (location, notifications, etc.)
iosOptions.setCapability("autoAcceptAlerts", true);

// Use native screenshot instead of Appium's
iosOptions.setCapability("nativeScreenshot", true);

// Timeout for WDA startup
iosOptions.setCapability("wdaLaunchTimeout", 120000);
iosOptions.setCapability("wdaConnectionTimeout", 120000);

The wdaLaunchTimeout is often the cause of session creation failures on iOS. WebDriverAgent (WDA) has to install and start on the device before Appium can control it. On first run for a device, this can take 60–90 seconds. Default timeout is often too low.

Parallel Execution Across Multiple Devices

Parallel execution is where the real value of a device cloud becomes clear. Instead of running 100 test scenarios sequentially across 3 hours, you fan them out across 10 devices and finish in 18 minutes.

TestNG Parallel Configuration

<!-- testng-parallel.xml -->
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Parallel Mobile Tests" parallel="tests" thread-count="5">

    <test name="Samsung S22 Android 12">
        <parameter name="deviceName" value="Samsung Galaxy S22_12.0_"/>
        <parameter name="platformVersion" value="12.0"/>
        <classes>
            <class name="com.company.tests.LoginTest"/>
            <class name="com.company.tests.CheckoutTest"/>
        </classes>
    </test>

    <test name="Pixel 6 Android 13">
        <parameter name="deviceName" value="Google Pixel 6_13.0_"/>
        <parameter name="platformVersion" value="13.0"/>
        <classes>
            <class name="com.company.tests.LoginTest"/>
            <class name="com.company.tests.CheckoutTest"/>
        </classes>
    </test>

    <test name="Xiaomi Redmi Note 11 Android 12">
        <parameter name="deviceName" value="Xiaomi Redmi Note 11_12.0_"/>
        <parameter name="platformVersion" value="12.0"/>
        <classes>
            <class name="com.company.tests.LoginTest"/>
            <class name="com.company.tests.CheckoutTest"/>
        </classes>
    </test>

</suite>

Thread-Safe Driver Management

With parallel tests, each thread needs its own driver instance. Use ThreadLocal:

public class DriverManager {
    private static final ThreadLocal<AndroidDriver> driverThread = new ThreadLocal<>();

    public static AndroidDriver getDriver() {
        return driverThread.get();
    }

    public static void setDriver(AndroidDriver driver) {
        driverThread.set(driver);
    }

    public static void removeDriver() {
        driverThread.remove();
    }
}

public class BaseTest {

    @Parameters({"deviceName", "platformVersion"})
    @BeforeMethod
    public void setUp(String deviceName, String platformVersion) throws Exception {
        UiAutomator2Options options = new UiAutomator2Options();
        options.setCapability("pCloudy_Username", System.getenv("PCLOUDY_USERNAME"));
        options.setCapability("pCloudy_ApiKey", System.getenv("PCLOUDY_API_KEY"));
        options.setCapability("pCloudy_DeviceFullName", deviceName);
        options.setCapability("pCloudy_DeviceVersion", platformVersion);
        options.setCapability("pCloudy_DurationInMinutes", "30");
        options.setCapability("pCloudy_ApplicationName",
            System.getenv("PCLOUDY_APP_PATH"));
        options.setCapability("appPackage", "com.company.app");
        options.setCapability("appActivity", "com.company.app.MainActivity");
        options.setCapability("automationName", "UiAutomator2");
        options.setCapability("autoGrantPermissions", true);

        AndroidDriver driver = new AndroidDriver(
            new URL("https://device.pcloudy.com/appiumcloud/wd/hub"),
            options
        );
        DriverManager.setDriver(driver);
    }

    @AfterMethod
    public void tearDown() {
        AndroidDriver driver = DriverManager.getDriver();
        if (driver != null) {
            driver.quit();
        }
        DriverManager.removeDriver();
    }
}

Run with:

mvn test -DsuiteXmlFile=testng-parallel.xml

With 5 devices running in parallel, TestNG starts 5 threads simultaneously, each creating its own pCloudy session on a different device. Device minutes consumed = sum of all session durations across all devices.

Native Framework Testing: Espresso

Espresso runs faster than Appium for Android because it executes in the same process as the app — no network round-trips for each command. pCloudy supports Espresso via its native testing API.

Upload App and Test APK

# Upload main app
APP_RESULT=$(curl -X POST "https://device.pcloudy.com/api/upload_file" \
  -F "file=@./app/build/outputs/apk/debug/app-debug.apk" \
  -F "username=$PCLOUDY_USERNAME" \
  -F "access_key=$PCLOUDY_API_KEY" \
  -F "source_type=raw")

APP_PATH=$(echo $APP_RESULT | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['file'])")

# Upload test APK
TEST_RESULT=$(curl -X POST "https://device.pcloudy.com/api/upload_file" \
  -F "file=@./app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk" \
  -F "username=$PCLOUDY_USERNAME" \
  -F "access_key=$PCLOUDY_API_KEY" \
  -F "source_type=raw")

TEST_PATH=$(echo $TEST_RESULT | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['file'])")

Trigger Espresso Run via API

curl -X POST "https://device.pcloudy.com/api/start_espresso" \
  -H "Content-Type: application/json" \
  -d "{
    \"username\": \"$PCLOUDY_USERNAME\",
    \"access_key\": \"$PCLOUDY_API_KEY\",
    \"app_file\": \"$APP_PATH\",
    \"test_file\": \"$TEST_PATH\",
    \"device_full_name\": \"Samsung Galaxy S22_12.0_\",
    \"duration\": 30,
    \"test_runner\": \"androidx.test.runner.AndroidJUnitRunner\"
  }"

Espresso runs complete faster and produce more reliable results than Appium for the same test scenarios. The trade-off: you can only test Android, and your test code must be written in Java/Kotlin as an Android instrumentation test.

Native Framework Testing: XCUITest

For iOS, pCloudy supports XCUITest via a similar API — upload your app and test runner, trigger the run, poll for results.

# Trigger XCUITest run
curl -X POST "https://device.pcloudy.com/api/start_xcuitest" \
  -H "Content-Type: application/json" \
  -d "{
    \"username\": \"$PCLOUDY_USERNAME\",
    \"access_key\": \"$PCLOUDY_API_KEY\",
    \"app_file\": \"$IPA_PATH\",
    \"test_file\": \"$TEST_RUNNER_PATH\",
    \"device_full_name\": \"Apple iPhone 14_16.2_\",
    \"duration\": 30,
    \"test_class\": \"MyAppUITests\"
  }"

XCUITest on real devices via pCloudy is considerably more reliable than using iOS simulators for anything hardware-adjacent. Bluetooth, camera, Touch ID, and real cellular network behavior all require physical hardware.

Performance Data Collection

pCloudy can capture device performance metrics during test runs:

options.setCapability("pCloudy_EnablePerformanceData", "true");

With this enabled, each test session report includes:

  • CPU usage over time
  • Memory consumption
  • Network data transferred
  • Battery drain

This is useful for performance regression testing — if a new build suddenly uses 40% more CPU during the checkout flow, you want to know before shipping.

Handling Flakiness

Real device tests on cloud platforms have inherent sources of flakiness:

  • Network latency between your commands and the device
  • Device initialization time variability
  • App state from previous sessions

Mitigation strategies:

// Increase implicit wait for remote device latency
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

// Use explicit waits instead of Thread.sleep
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30));
WebElement element = wait.until(
    ExpectedConditions.elementToBeClickable(By.id("com.company.app:id/button"))
);

// Retry on StaleElementReferenceException
public WebElement findElementWithRetry(By locator, int maxAttempts) {
    for (int i = 0; i < maxAttempts; i++) {
        try {
            return driver.findElement(locator);
        } catch (StaleElementReferenceException e) {
            if (i == maxAttempts - 1) throw e;
        }
    }
    throw new RuntimeException("Element not found after " + maxAttempts + " attempts");
}

The pCloudy platform itself retries session creation internally if device allocation fails on the first attempt. For your test code, build in explicit waits and retry logic for the commands most likely to be timing-sensitive.

Read more

Start now free