iOS and Android app tests
Upload a build, drive it on a cloud device in your browser, and replay every tap on each release.
Mobile tests work the way web tests do, with one difference at the start: instead of pointing at a URL, you upload a build of your app. Testsnag runs it on a cloud device streamed into the recorder, captures your taps and swipes as steps, and replays them on every run.
- 1Pick iOS App or Android App. The dialog swaps the target URL for build fields.
- 2Choose the build from your Files library. Only files that could be a build for this platform are listed.
- 3The hint changes per platform: a zipped simulator build for iOS, an .apk or .apks for Android.
- 4The device the test runs on.
- 5And the OS version.
- 6Create and the build starts publishing straight away, so the test is ready in one step.
Uploading a build
This is the step that trips people up, and the two platforms differ. Get it right and everything after is the same on both.
- iOS: upload a zipped simulator build. Produce it with `xcodebuild -sdk iphonesimulator` (or `expo run:ios`), then zip the .app bundle. An App Store or TestFlight .ipa is a device build and cannot run on a simulator, so Testsnag rejects it with the command you need.
- Android: upload the .apk or .apks you already ship. No rebuild is needed. An .aab is not accepted directly; convert it first with Google’s bundletool and Testsnag tells you the exact command.
Builds come from your Files library, not from the test itself. Upload each release to Files once, then point as many tests at it as you need. You pick the build, the device, and the OS version in the create dialog, so the test is ready the moment it exists. To swap in a new release later, or to change the device, OS version, or orientation, use the App build tab in the test’s settings; the recorder itself stays a recorder.
The picker only lists files that could be a build for that platform: .zip, .tar.gz and .tgz for iOS, .apk and .apks for Android. If the list is empty, upload the build on the Files page first.
Flutter, React Native, and other cross-platform apps
Every cross-platform framework ends up generating an Xcode project for iOS and a Gradle project for Android, so the rule is the same as a native app: Android takes the .apk you already produce, and iOS needs a simulator build rather than the .ipa you ship to the App Store. Only the command changes.
- Flutter: `flutter build ios --simulator` writes build/ios/iphonesimulator/Runner.app, and `flutter build apk` writes build/app/outputs/flutter-apk/app-release.apk.
- React Native: `npx react-native run-ios` leaves the app in ios/build/Build/Products/Debug-iphonesimulator, and `./gradlew assembleRelease` in the android directory produces the .apk.
- Expo: `eas build -p ios --profile preview` with "simulator": true in eas.json. This is the easiest case, because EAS hands you a .tar.gz that already contains the .app and needs no zipping.
- Capacitor and Ionic: build the generated ios/App project with `xcodebuild -sdk iphonesimulator`, and Android with `./gradlew assembleRelease`.
- .NET MAUI and Xamarin: `dotnet build -f net9.0-ios -r iossimulator-arm64` for iOS, and `dotnet build -f net9.0-android` for Android.
- Unity and Kotlin Multiplatform: export the Xcode and Gradle projects, then build them exactly as a native app.
On iOS most of these hand you a .app directory rather than a file, so there is one more step before uploading. Expo is the exception and needs none.
cd build/ios/iphonesimulator && zip -r Runner.zip Runner.appThe command line tool looks in all of these output directories, so it usually finds a fresh build without you typing a path.
Publishing a build to the device cloud takes a moment. The recorder shows a publishing state and the device becomes available once it finishes. If publishing fails, the App build tab explains why and offers Retry publishing, so a transient problem does not mean re-uploading the file.
No target URL, a launch URL instead
A web test starts by navigating to a target URL. A mobile test has nothing to navigate to, so that field is replaced by an optional Launch URL: a deep link into your own app, written in your app’s own scheme, such as myapp://home. Leave it empty and the test starts wherever the app opens by default. Set it and every run begins on the same screen, which saves recording the same three taps at the top of every test.
Orientation
Choose portrait or landscape on the App build tab. The device starts in that orientation for recording and for every run, so a landscape-only game or a tablet layout is tested the way it is actually used rather than rotated by hand each time.
Build details
The App build tab also shows what the device cloud actually read out of the file you uploaded: the bundle identifier, the version name and code, and the platform. It is the quickest way to confirm you published the build you meant to, which matters most when two releases have the same filename. Refresh it if you have just published a new build.
Recording on a device
The recorder opens on the same start screen as the web recorder. Use AI boots the device with the AI chat panel open, Use Device boots it for you to drive directly, and Add Step lets you write steps by hand. Once the device is running, the Use AI and Use Device switch in the header flips between the two. Stop in the device bar ends the session and returns you to the start screen.
If the test already has steps, they are replayed first before recording resumes, so a new session picks up where the last one finished rather than starting from a cold app. The steps list shows the replay advancing and marks anything that could not be replayed, so you find out immediately if an earlier step has stopped working.
The device itself is driven from a floating window you can move around the page, and it can be expanded to fill the screen, where the device is centred. Every tap, swipe, and keystroke becomes a step, and Testsnag stores the element it resolved rather than only the coordinates, so a step keeps working when the layout shifts. Consecutive keystrokes are collapsed into a single type step rather than one step per character.
Driving the device with AI
Mobile recording is built around the AI chat panel. Describe what you want in plain English and Testsnag reads the screen, works out which element you mean, performs the action on the device in front of you, and records it as a step. You can still tap the device directly in the floating window whenever it is quicker to show than to describe. The split-view mode offered on web is not used here, because the device stream needs a window of its own to stay undistorted.
Assertions match on accessibility identifiers where your app provides them, falling back to visible text. Apps with good accessibility identifiers produce far more durable tests, which is worth knowing before you invest in a large suite.
Waiting for a device
Devices are a shared pool. If they are all busy, the recorder shows your position in the queue rather than failing. Sessions also pause after a period of inactivity, with a warning and a control to keep the session alive.
Shipping a new build
Swapping in a new release from the dashboard means visiting each test in turn. The command line tool does it in one command, binds the build to as many tests as you like, and runs from a build pipeline so a broken release never ships.
Device minutes
Mobile testing is metered in streamed device minutes as well as runs, and both recorder time and run time count. iOS and Android draw from one pool. When the monthly allowance is used up, new mobile runs are refused until it resets or you move up a plan; everything else keeps working.
What a run produces
A mobile run reports like any other: a pass or fail, a screenshot for every step, console and network logs, and a video of the device you can watch back. Playback skips the boot and app launch at the head of the file and opens on the first frame of your app. Read a mobile run the same way as any other run, watch the video from the three-dot menu, and cancel it from the same place.
Still stuck, or spotted something out of date?
Contact support