# Mobile Pull Request Testing: How to Test a Native App PR Without a Preview URL

> Native iOS and Android pull requests don't get a preview URL. How to test the PR's own build before merge: what CI uploads, how the run is pinned to it, and what to check.

Source: https://qa.tech/blog/mobile-pull-request-testing · Published: 2026-11-06

---
A web pull request gets a preview deployment, and anything that can open a URL can test it. A native iOS or Android pull request gets nothing to open, so mobile pull request testing starts in CI: it builds a simulator or emulator binary from the PR's commit and uploads it, and the tests run against that exact file.

## Why mobile changes reach main untested

On a lot of mobile teams, nobody runs the app until the release candidate exists. The CTO of a consumer health app on React Native walked us through their routine: a release every four to six weeks, a code freeze before each one, then a couple of days of manual testing across platforms.

Code review still happens on every PR. But a reviewer reads the diff, and the diff can't show you a button that now sits under the keyboard on a small Android screen. So the bugs pile up until the code freeze, where they all arrive at once and nobody remembers which PR caused which.

Google's own Android guidance draws the line in a different place. In the example infrastructure on its [testing strategies page](https://developer.android.com/training/testing/fundamentals/strategies), feature tests run "pre-merge" on "local and emulators", and the full device matrix, eight phones plus a foldable and a tablet, is saved for release candidates.

## What "the PR build" is on iOS and Android

For automated testing you want a build that runs on a virtual device and needs no signing.

- **iOS:** a simulator build, which is a `.app` folder. [Bitrise's docs](https://docs.bitrise.io/en/bitrise-ci/testing/testing-ios-apps/building-an-ios-app-for-a-simulator) note that it "requires no code signing at all." Zip the `.app` before you upload it.
- **Android:** an `.apk`. If your pipeline only produces an `.aab` for Google Play, use bundletool's universal mode to get a single `.apk` out of it.

An `.ipa` or a TestFlight build won't do here. [Apple's docs](https://developer.apple.com/documentation/xcode/distributing-your-app-to-registered-devices) say testers "can run your app only on the devices that you add to the provisioning profile", so a cloud simulator can't use it. We've written up [which build format to use for automated testing](https://qa.tech/blog/mobile-build-for-automated-testing) in more detail, including the Expo and Bitrise settings.

## The pipeline, step by step

1. A PR is opened or updated, which fires a `pull_request` workflow.
2. CI builds the simulator `.app` (on a macOS runner) or the `.apk` (Linux is fine).
3. CI uploads the file to your test platform and gets back an ID for that build.
4. CI starts the test run or review and pins it to that build ID.
5. The verdict lands on the PR as a review or a check, and the job can wait for it if you want it to gate the merge.

Here's the shape of it in GitHub Actions for Android, trimmed from [our docs](https://docs.qa.tech/pr-testing/github#mobile-build):

```yaml
name: Mobile PR review
on:
  pull_request:

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build APK
        run: ./gradlew assembleDebug
      - name: Upload build
        id: upload
        run: ./scripts/upload-build.sh app/build/outputs/apk/debug/app-debug.apk
      - uses: QAdottech/run-action/change-review@v4
        with:
          project_short_id: 'proj_abc123'
          api_token: ${{ secrets.QATECH_API_TOKEN }}
          blocking: true
          applications_config: |
            {"applications": {"app_gXeBl2": {"environment": {
              "applicationBuildShortId": "${{ steps.upload.outputs.build_short_id }}"}}}}
```

The upload script is three HTTP calls: get an upload URL, `PUT` the file, register the build. The full version is on [API pull request testing](https://docs.qa.tech/pr-testing/api#upload-the-pr-build). iOS is the same flow on a `macos-26` runner, with a simulator build zipped before upload.

If you build on Bitrise or fastlane, add the upload and review call as a step after the build step. Bitrise exposes the APK as `$BITRISE_APK_PATH` and the simulator app as `$BITRISE_APP_DIR_PATH`, which you zip before upload. A review started from Bitrise or fastlane doesn't block the build, so if you need a hard gate in either, run a test plan instead.

## Three things that trip teams up

**The tests ran, just against the wrong build.** If an automatic PR integration starts a run without a build override, it tests whatever build is stored on the environment, which is usually whatever someone uploaded last week. Pin every PR run to the uploaded build, and turn automatic runs off for that repository when CI owns the trigger. Then promote each merged build to the environment from CI on every push to main, so scheduled runs and ad-hoc checks test current code too.

**The app is new and the backend isn't.** The PR binary still calls whichever API it was built against, so a PR that relies on a new backend route can't pass against a backend that doesn't have it yet. If the PR spans app and API, point the build at a preview backend for that PR, or test it after the backend change has merged.

**The firewall says no.** Mobile test devices in the cloud usually come from a different IP range than web test browsers. If login fails on every run and works on your phone, check the allowlist before you check the code.

## What to test on a mobile PR

Test what the change can reach. A PR that edits the checkout sheet needs checkout on both platforms, plus anything that reuses the same component. It doesn't need the whole app. How deep to go is a [risk-based testing](https://qa.tech/blog/risk-based-testing-pull-requests) call: a copy change gets a quick check, and a change to auth or payments gets more.

Keep a small regression suite of the flows that must never break, like sign-up, login and payment, and run it on main on a schedule. Everything else is better covered per change, on the build that contains it. We've made [the longer argument for keeping that suite small](https://qa.tech/blog/large-regression-suite-is-a-liability) elsewhere.

Camera, NFC, biometrics and real performance all need a physical device to test properly, so they belong in the release-candidate pass. There's a fuller breakdown in [emulator vs real device testing](https://qa.tech/blog/emulator-vs-real-device-testing).

## How QA.tech reviews a mobile pull request

QA.tech is an AI testing platform that tests pull requests before merge, runs regression test plans on main after it, and covers web, mobile and API in one place. On a mobile PR it takes the build your CI uploaded and reviews the change on that binary, on cloud iOS Simulators and Android Emulators, so the hardware checks above stay in your device pass. The agent reads the PR description, linked tickets, changed files and commits, reruns the test cases that already cover the change and writes new ones for anything uncovered. It then posts a native GitHub review whose verdict is pass, fail, or unable to verify. GitLab merge requests, Bitrise and fastlane use the same upload and review call.

A change that touches both your mobile app and your web app gets simulators and browsers in the same review, so one verdict covers the whole user journey.

Each test case is a short description of the flow, and the agent locates elements from the screen itself, so your developers don't have to add test IDs first. Teams with near-identical iOS and Android apps keep both builds in one application and run one set of cases against each.

![Example QA.tech pull request review with a verdict and test results](https://qa.tech/blog-assets/mobile-pull-request-testing-pr-result.jpg)
*A QA.tech review posted on a web pull request. A review pinned to an uploaded iOS or Android build appears on the PR with the same verdict and test list.*

Mobile builds are tested on the Growth and Enterprise plans ([mobile testing](https://qa.tech/product/mobile-testing) has the details). If you ship a web app too, the free trial lets you try PR review on that first.

## FAQ

**Can you test a mobile pull request without a preview URL?**
Yes. Build a simulator `.app` or an `.apk` from the PR's commit in CI, upload it, and pin the test run to that build. The binary plays the role a preview URL plays on the web.

**Do I need a new native build for every pull request?**
For native iOS and Android code, yes, because the binary is what changed. React Native and Expo teams can sometimes reuse the native layer when only JavaScript changed, but the test binary still has to contain the PR's JavaScript. We cover that case in [React Native E2E testing on pull requests](https://qa.tech/blog/react-native-e2e-testing-pull-requests).

**Can I test a TestFlight build on each pull request?**
No. A TestFlight build has to be archived, signed and uploaded to App Store Connect before anyone can install it, and it only installs on physical devices. For a PR, build an unsigned simulator `.app` from the PR's commit instead.

**Should mobile PR tests block the merge?**
Start by posting results without blocking, and watch the false-positive rate for a few weeks. Once the team trusts the verdicts, make the check required on the flows that matter most.

**What if a pull request changes both the app and the backend?**
Test it against a backend that has the change. The PR build calls whichever API it was built against, so point it at a preview backend for that PR, or run the mobile tests after the backend change has merged.
