> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qa.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# fastlane regression testing

> Add a fastlane lane that uploads each new iOS or Android build and runs a test plan against it.

Start from a [test plan](https://docs.qa.tech/core-concepts/test-plans.md). A [fastlane](https://github.com/fastlane/fastlane) lane builds a simulator or emulator binary, uploads it, and runs that plan against it. The lane works on any CI that runs fastlane, or on your machine. For a change review of a pull request build, use [fastlane pull request testing](https://docs.qa.tech/pr-testing/fastlane.md).

## Set it up

> [card] [Add the lane](#add-the-lane)
>
> Copy the `Fastfile` helpers and the Android and iOS lanes.

> [card] [Run it](#run-it)
>
> Run the lane after each new build, locally or in CI.

> [!TIP]
>
> New to mobile app testing on QA.tech? Start with [Mobile App
> Testing](https://docs.qa.tech/test-features/mobile-app-testing.md) to create your mobile application
> first.

**1. Store the API token**

Create a token in **Organization Settings → API Keys**. Store it as `QATECH_API_TOKEN` in your CI secrets, or in a `fastlane/.env` file you do not commit.

**2. Get the application short ID**

Copy `app_…` from **Settings → Applications & Envs** (for example
`app_gXeBl2`) and store it as `QATECH_APP_ID`. See [Finding Short
IDs](https://docs.qa.tech/core-concepts/applications-and-environments.md#finding-short-ids-for-api-usage).

**3. Get the test plan short ID**

Open the plan under [Test
Plans](https://app.qa.tech/current-project/test-plans) and copy `pln_…` from
the URL (for example `pln_abc123`). Store it as `QATECH_TEST_PLAN_ID`. See
[Find the test plan short
ID](https://docs.qa.tech/core-concepts/test-plans.md#find-the-test-plan-short-id).

**4. Set the project short ID for org-scoped keys**

A project-scoped key is bound to one project and needs nothing else. With an
organization-scoped key, copy `proj_…` from the project URL or Settings and
store it as `QATECH_PROJECT_ID`. The lane sends it as `projectShortId` on
every request. See [Understanding Different
IDs](https://docs.qa.tech/api-reference/introduction.md#understanding-different-ids).

## What works with fastlane

| Capability | fastlane |
| :- | :- |
| Review on a preview URL, before merge | No. See [fastlane pull request testing](https://docs.qa.tech/pr-testing/fastlane.md) for a change review of a pull request build. |
| Run a regression test plan from a lane | Yes. Upload the build and start the plan against it. |
| Run that plan against a mobile build | Yes. This is the primary fastlane path. |
| Block the lane until the plan finishes | Yes. The lane polls the run and fails unless the result is passed. |
| Schedule | On the test plan, with **Manage Schedules**. See [Manual and scheduled runs](https://docs.qa.tech/best-practices/running-tests.md#trigger-a-run-on-a-schedule). |

## What this does not do

* It does not post a GitHub or GitLab review. See [fastlane pull request testing](https://docs.qa.tech/pr-testing/fastlane.md).
* It does not test store artifacts. Upload a simulator or emulator build of the same commit, not the `.ipa` or `.aab` you send to the stores.
* It does not replace a test plan schedule. **Manage Schedules** runs the plan without a new build.

## Add the lane

Add this to `fastlane/Fastfile`. The helpers call the [Application Builds API](https://docs.qa.tech/api-reference/application-builds.md) and [Start Run](https://docs.qa.tech/api-reference/runs/start-test-run.md) in four parts:

1. Get a presigned upload URL
2. Upload the build file directly to storage
3. Create the build record
4. Start a test run pinned to that build, then poll it until it finishes

Replace the workspace, scheme, and app name with your own.

```ruby fastlane/Fastfile theme={null}
require "json"
require "net/http"
require "uri"

QATECH_API = "https://api.qa.tech/v1"

def qatech_request(method, path, body = nil)
  uri = URI("#{QATECH_API}#{path}")
  request = (method == :post ? Net::HTTP::Post : Net::HTTP::Get).new(uri)
  request["Authorization"] = "Bearer #{ENV.fetch('QATECH_API_TOKEN')}"
  request["Content-Type"] = "application/json"
  if body
    # Org-scoped API keys need the project on every request.
    body = body.merge(projectShortId: ENV["QATECH_PROJECT_ID"]) if ENV["QATECH_PROJECT_ID"]
    request.body = body.to_json
  end
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
  UI.user_error!("QA.tech #{path} failed (#{response.code}): #{response.body}") unless response.is_a?(Net::HTTPSuccess)
  JSON.parse(response.body)
end

# Uploads a build and returns its applicationBuildShortId.
def qatech_upload_build(file:, platform:)
  app_id = ENV.fetch("QATECH_APP_ID")
  upload = qatech_request(:post, "/applications/#{app_id}/builds/upload-url", { fileName: File.basename(file) })

  uri = URI(upload["uploadUrl"])
  File.open(file, "rb") do |io|
    put = Net::HTTP::Put.new(uri)
    put["Content-Type"] = "application/octet-stream"
    put["Content-Length"] = File.size(file).to_s
    put.body_stream = io
    response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(put) }
    UI.user_error!("Build upload failed (#{response.code})") unless response.is_a?(Net::HTTPSuccess)
  end

  build = qatech_request(:post, "/applications/#{app_id}/builds", { platform: platform, buildToken: upload["buildToken"] })
  UI.success("Uploaded #{build['applicationBuildShortId']}")
  build["applicationBuildShortId"]
end

# Starts the test plan on the build and fails the lane unless the run passes.
def qatech_run_test_plan(build_short_id:, wait: true)
  run = qatech_request(:post, "/run", {
    testPlanShortId: ENV.fetch("QATECH_TEST_PLAN_ID"),
    applications: [{
      applicationShortId: ENV.fetch("QATECH_APP_ID"),
      environment: { applicationBuildShortId: build_short_id }
    }]
  })
  run_id = run["run"]["shortId"]
  UI.message("Started QA.tech run #{run_id}: #{run['run']['url']}")
  return run_id unless wait

  loop do
    status = qatech_request(:get, "/run/#{run_id}")
    if %w[COMPLETED ERROR CANCELLED].include?(status["status"])
      UI.user_error!("QA.tech run #{run_id} finished with #{status['result']}") unless status["result"] == "PASSED"
      UI.success("QA.tech run #{run_id} passed")
      return run_id
    end
    sleep(30)
  end
end

platform :android do
  lane :qatech do
    gradle(task: "assemble", build_type: "Debug")
    build_id = qatech_upload_build(
      file: lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
      platform: "android"
    )
    qatech_run_test_plan(build_short_id: build_id)
  end
end

platform :ios do
  lane :qatech do
    root = File.expand_path("..", Dir.pwd) # lanes run from fastlane/
    derived_data = File.join(root, "build")

    # Simulator build: device and App Store .ipa files cannot run on simulators.
    sh("xcodebuild", "-workspace", File.join(root, "MyApp.xcworkspace"),
       "-scheme", "MyApp", "-configuration", "Debug",
       "-sdk", "iphonesimulator", "-destination", "generic/platform=iOS Simulator",
       "-derivedDataPath", derived_data, "build")

    products = File.join(derived_data, "Build/Products/Debug-iphonesimulator")
    sh("cd '#{products}' && zip -qr MyApp.zip MyApp.app")

    build_id = qatech_upload_build(file: File.join(products, "MyApp.zip"), platform: "ios")
    qatech_run_test_plan(build_short_id: build_id)
  end
end
```

> [!NOTE]
>
> Supported file types are `.apk` and `.aab` for Android, and `.zip` or
> `.tar.gz` containing your `.app` simulator build for iOS. Maximum file size is
> 4GB. See [Preparing Your App
> Build](https://docs.qa.tech/test-features/mobile-app-testing.md#preparing-your-app-build) for
> simulator builds, including Expo and React Native.

## Run it

Run the lane after each new build:

```bash theme={null}
bundle exec fastlane android qatech
```

```bash theme={null}
bundle exec fastlane ios qatech
```

The lane fails when the run does not pass, so CI marks the build as failed. Pass `wait: false` to `qatech_run_test_plan` to start the run and continue without waiting; the results still reach your [notification channels](https://docs.qa.tech/core-concepts/notifications.md). If polling might exceed your CI job timeout, raise the timeout or pass `wait: false`.

To test a build in an existing lane (for example before `upload_to_play_store` or `upload_to_testflight`), call `qatech_upload_build` and `qatech_run_test_plan` from that lane.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.