Skip to main content

Native Mobile (Incubating)

TestFly can drive native Android and iOS apps without Appium. It talks to mobilecli, a server that you install; TestFly does not bundle, download or modify it, but it can start your installed binary when the server is not running. The API is inspired by Mobilewright. Design and decisions: ADR 0002 (docs/adr/0002-native-mobile-via-mobilecli.md).

Incubating

Everything in io.testfly.mobile is incubating: it is not annotated @TestFlyApi and may change in a minor release. The Mobile Emulator Smoke workflow runs the examples on Android emulators (API 30 and 34) and an iOS simulator on every relevant change; real phones are not covered by CI yet.

PageWhat it covers
This pagesetup, the first test on TestNG, JUnit 5 and Cucumber, ready-made examples
Locators and page objectsgetBy… locators, selector strings, MobilePage/@MobileFind/@WaitFor, YAML locator files, preconditions (@PreCondition with MobileConditions), actions and assertions
Configurationthe mobile: block, device selection, device pool, app reset, server auto-start, profiles, security
CIrunning on GitHub Actions with an Android emulator or an iOS simulator
Troubleshootingexceptions, common problems, limits

Prerequisites​

  1. Install mobilecli 1.0.20 or newer, for example npm install -g mobilecli. Android needs the Android SDK (adb); iOS simulators need Xcode on macOS.

  2. Boot an emulator or simulator, or connect a phone, and find its id:

    mobilecli devices

    On an Android emulator the id is the AVD name (for example Pixel_8_API_34), not the adb serial (emulator-5554). On iOS it is the simulator UDID.

  3. iOS: install mobilecli's agent on the device once; screen reads and taps need it. Android needs no agent (agent status just reports that):

    mobilecli agent install --device <device-id>
    mobilecli agent status --device <device-id>

    A real iPhone needs the agent signed for it: mobilecli agent install --device <udid> --provisioning-profile /path/to/profile.mobileprovision (a valid Apple provisioning profile). Webviews and Safari tabs on a real device also need Settings > Apps > Safari > Advanced > Web Inspector on. File push/pull is not available on real iPhones.

You do not need to start the server yourself: when http://localhost:12000 does not answer, TestFly runs mobilecli server start and stops it when the JVM exits (see server auto-start). Starting it yourself (mobilecli server start) also works.

Minimal configuration​

Nothing mobile loads unless the active testfly.yml (or profile, -Dtestfly.profile=<name> → testfly-<name>.yml) has a mobile: section:

mobile:
platform: ios # android | ios
deviceId: ${MOBILE_DEVICE_ID:-} # empty: the only online device of the platform
app:
bundleId: com.example.app # iOS bundle id / Android package

All other keys have defaults; see Configuration. A mobile-only profile needs no browser, execution or timeouts block.

Your first test​

EngineHow a test becomes a mobile test
TestNGextend io.testfly.mobile.BaseMobileTest, or put @MobileTest on a class or method that already runs with TestFly's listeners (for example in a BaseTest subclass)
JUnit 5extend io.testfly.mobile.junit5.BaseJUnit5MobileTest, or put @MobileTest on a class or method that uses TestFlyExtension
Cucumbertag the scenario or feature @mobile; step classes implement MobileSupport

For a mobile test, TestFly skips the browser, opens the device before the test and closes it afterwards. The device belongs to the test's thread.

With page objects, a test reads as one chain:

public class LoginTest extends BaseMobileTest {

@Test
public void signsIn() {
new HomePage()
.openLogin()
.signIn(System.getenv("MOBILE_LOGIN_USER"), System.getenv("MOBILE_LOGIN_PASSWORD"))
.shouldBeClosed();
}
}

Direct locators work too, for short checks:

screen().getByTestId("username").fill("ada");
screen().getByRole(MobileRole.BUTTON, "Sign in").tap();
expect(screen().getByText("Hello, ada")).toBeVisible();
apiClient().get("/me").send().assertStatus(200); // backend check in the same test

The base classes also provide apiClient(), db(), mailbox(), ctx(), step(), softAssert() and getTestData().

Cucumber steps keep page instances between steps, because creating a page waits for its screen:

public class LoginSteps implements MobileSupport {
private HomePage home;
private LoginPage login;

@Given("the app is on the home screen")
public void onHome() { home = new HomePage(); }

@When("I open the login form")
public void openLogin() { login = home.openLogin(); }
}

Outside a test runner, open a device yourself:

try (MobileDevice device = Mobile.connect()) {   // reads testfly.yml
device.screen().getByText("Settings").tap();
}

Examples​

ExampleEngineRun
examples/mobile/IosSettingsExampleTestTestNG, iOS Settings appmvn test -Dtestfly.profile=mobile-ios -Dtest=IosSettingsExampleTest
examples/mobile/AndroidSettingsExampleTestTestNG, Android Settings appmvn test -Dtestfly.profile=mobile-android -Dtest=AndroidSettingsExampleTest
examples/mobile/IosLoginExampleTest, AndroidLoginExampleTestTestNG, page objects HomePage/LoginPage, @MobileTest(platform = …)MOBILE_LOGIN_USER=… MOBILE_LOGIN_PASSWORD=… mvn test -Dtest='IosLoginExampleTest,AndroidLoginExampleTest' (select testfly-mobile-login.yml, one profile with ios: and android: blocks)
examples/mobile/IosLoginJUnit5ExampleTest, AndroidLoginJUnit5ExampleTestJUnit 5, same pages and profileMOBILE_LOGIN_USER=… MOBILE_LOGIN_PASSWORD=… mvn test -Dtest='*LoginJUnit5ExampleTest'
examples/cucumber/mobile/IosLoginCucumberRunnerCucumber, features-mobile/ios_login.featuremvn test -Dtest=IosLoginCucumberRunner

All paths are under src/test/java/io/testfly/. The Settings examples need no app file; their texts assume an English device. Credentials come only from environment variables.