Skip to main content
Back to Blog
Guide
2026-09-28

Bitrise Testing for Mobile Test Automation: Workflows, Steps, and Test Reports

bitrise testing guide for mobile QA teams: build Android and iOS workflows, publish test reports, shard suites, and debug CI failures faster.

Bitrise Testing for Mobile Test Automation: Workflows, Steps, and Test Reports

Bitrise testing is best understood as mobile-first CI built around Steps, Workflows, and Pipelines. As of September 28, 2026, Bitrise CI is active, the docs are published at docs.bitrise.io, and the mobile testing Steps covered here have not been renamed into a different product. The important current Step IDs are android-unit-test, android-build-for-ui-testing, android-instrumented-test, virtual-device-testing-for-android, xcode-build-for-test, xcode-test-without-building, virtual-device-testing-for-ios, custom-test-results-export, and deploy-to-bitrise-io.

The direct answer: use Bitrise when your CI needs native Android and iOS stacks, device-oriented build Steps, code signing integrations, and one place to inspect artifacts and test reports. For a QA engineer, the payoff is less shell glue. You build APKs, test APKs, iOS test bundles, and simulator outputs with official Steps, then let the Deploy to Bitrise.io Step publish artifacts and feed the Test Reports add-on.

The catch is that Bitrise rewards explicit design. A single Workflow that runs every unit test, boots an emulator, builds an iOS test bundle, runs device tests, and uploads everything will work until it becomes slow and hard to diagnose. A better Bitrise testing setup separates fast feedback from device confidence, shares build outputs only when useful, and treats Test Reports as a contract. If you are also designing real-device coverage, pair this with mobile device farm testing strategy. If your mobile UI layer is Appium-heavy, compare the Bitrise device path with the broader Appium mobile testing complete guide.

The Bitrise Objects QA Engineers Actually Configure

Bitrise configuration lives in bitrise.yml. The current YAML reference describes format_version, default_step_lib_source, project_type, workflows, and pipelines as top-level concepts. Workflows are ordered Step lists. Pipelines compose Workflows and can run them in dependency graphs or stages. Steps are reusable units, usually referenced by Step ID and optional major version.

ObjectWhat it ownsTesting decision it controls
StepOne task, such as cloning, running Android unit tests, or exporting resultsWhich official integration runs and which inputs are passed
WorkflowSequential Step listThe lifecycle for one type of validation, such as unit tests or UI tests
PipelineMultiple Workflows with dependencies or stagesParallelism, fan-out, and build artifact reuse
Env VarShared or workflow-scoped valueModule, variant, scheme, destination, shard count, and secrets
Stack and machineBuild image and resourcesXcode version, Android SDK availability, CPU, memory, and emulator reliability

A minimal Android unit-test Workflow is deliberately boring. The value is that the Android Unit Test Step knows where Gradle test reports normally land and can export them for Bitrise Test Reports when followed by deploy-to-bitrise-io.

format_version: '25'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
project_type: android

app:
  envs:
  - PROJECT_LOCATION: .
    opts:
      is_expand: false
  - MODULE: app
    opts:
      is_expand: false
  - VARIANT: debug
    opts:
      is_expand: false

workflows:
  android_unit_tests:
    steps:
    - git-clone: {}
    - android-unit-test@1:
        inputs:
        - project_location: ${PROJECT_LOCATION}
        - module: ${MODULE}
        - variant: ${VARIANT}
    - deploy-to-bitrise-io@2: {}

Notice the job boundary: this does not build a release APK, run instrumentation tests, or publish an installable app. That is intentional. Fast unit feedback should fail quickly and produce reports that are easy to read.

Android Unit, Instrumented, and Virtual Device Tests

Bitrise documents two different Android paths that teams often merge by accident. Local unit tests run on the JVM through the Android Unit Test Step. Instrumented tests need an APK plus a test APK, then a runner on a device or emulator. For instrumented tests on Bitrise, the official docs describe Android Build for UI testing as the Step that builds both artifacts and exports their paths as BITRISE_APK_PATH and BITRISE_TEST_APK_PATH. The Android Instrumented Test Step can consume those paths for a local emulator path, while Virtual Device Testing for Android runs against Firebase Test Lab infrastructure without requiring your own Firebase account.

Test typePrimary Step sequenceBest useReport path concern
JVM unit testsandroid-unit-test -> deploy-to-bitrise-ioFast validation of Kotlin or Java logicDefault Gradle XML and HTML outputs usually work
Local emulator instrumentationavd-manager -> android-build-for-ui-testing -> wait-for-android-emulator -> android-instrumented-test -> export -> deployDebuggable emulator runs inside one WorkflowExport XML from Android test result directories
Firebase virtual device testsandroid-build-for-ui-testing -> virtual-device-testing-for-android -> deploy-to-bitrise-ioBroader device matrix with managed device executionStep can export results to Test Reports
Robo or gameloop testsandroid-build-for-ui-testing -> virtual-device-testing-for-androidSmoke exploration when no full instrumentation suite existsTreat failures as signals, not precise assertions

Here is a practical virtual-device Workflow. The Bitrise docs note that the Android virtual device testing solution has a maximum duration, configurable by the Test timeout input, and one Virtual Device Testing Step in a single build can perform one test type. Keep that limitation in mind before adding every model you can find.

workflows:
  android_virtual_device_tests:
    steps:
    - git-clone: {}
    - android-build-for-ui-testing@0:
        inputs:
        - project_location: ${BITRISE_SOURCE_DIR}
        - module: app
        - variant: debug
        - apk_path_pattern: '*/build/outputs/apk/*.apk'
    - virtual-device-testing-for-android@1:
        inputs:
        - test_type: instrumentation
        - app_path: ${BITRISE_APK_PATH}
        - test_apk_path: ${BITRISE_TEST_APK_PATH}
        - test_devices: Pixel2,28,en,portrait
    - deploy-to-bitrise-io@2: {}

If you need local emulator control, use the emulator path instead. It is slower to provision, but it gives you more direct access to logs and can be easier to reproduce locally.

workflows:
  android_local_emulator_tests:
    steps:
    - avd-manager@1: {}
    - git-clone: {}
    - android-build-for-ui-testing@0:
        inputs:
        - project_location: ${BITRISE_SOURCE_DIR}
        - module: app
        - variant: debug
    - wait-for-android-emulator@1: {}
    - android-instrumented-test@0:
        inputs:
        - main_apk_path: ${BITRISE_APK_PATH}
        - test_apk_path: ${BITRISE_TEST_APK_PATH}
        - test_runner_class: androidx.test.runner.AndroidJUnitRunner
    - custom-test-results-export@1:
        inputs:
        - test_name: Emulator tests
        - base_path: ${BITRISE_SOURCE_DIR}/app/build/outputs/androidTest-results
        - search_pattern: '*.xml'
    - deploy-to-bitrise-io@2: {}

What people get wrong: they treat android-build-for-ui-testing as a generic build Step. It is specifically valuable because downstream test Steps understand its artifacts. If your Workflow builds the app with an unrelated script and then guesses APK paths, your setup becomes fragile the first time Gradle output paths change or a flavor adds another artifact.

iOS Xcode Tests, Build-for-Testing, and Device Runs

iOS on Bitrise has two common shapes. The first is normal Xcode test execution with xcode-test style behavior through the Xcode Test for iOS Step. The second is build once, test later: xcode-build-for-test runs xcodebuild build-for-testing, creates a test bundle zip, and makes it available for later test execution. Bitrise docs also describe xcode-test-without-building for running prebuilt tests in parallel Pipelines.

For quick iOS validation, start with Xcode Test for iOS and Deploy to Bitrise.io. Bitrise documentation says this official Step generates an .xcresult file and can work with enhanced reporting without extra configuration. The Deploy Step then sends raw logs, artifacts, and test results to Bitrise.

workflows:
  ios_xcode_tests:
    steps:
    - git-clone: {}
    - xcode-test@4:
        inputs:
        - project_path: ${BITRISE_PROJECT_PATH}
        - scheme: ${BITRISE_SCHEME}
        - destination: platform=iOS Simulator,name=iPhone 15
    - deploy-to-bitrise-io@2: {}

For device testing through Firebase Test Lab for iOS, Bitrise docs say to add Xcode Build for testing for iOS, then iOS Device Testing. The Step ID shown in the configuration examples is xcode-build-for-test, and the iOS device testing Step ID is virtual-device-testing-for-ios. The build Step needs code signing if the app will be installed on devices. The docs list off, api-key, and apple-id as options for automatic code signing in that Step.

workflows:
  ios_device_tests:
    steps:
    - git-clone: {}
    - xcode-build-for-test@2:
        inputs:
        - project_path: ${BITRISE_PROJECT_PATH}
        - scheme: ${BITRISE_SCHEME}
        - configuration: Debug
        - destination: generic/platform=iOS
        - automatic_code_signing: api-key
    - virtual-device-testing-for-ios:
        inputs:
        - test_devices: iphone8,14.7,en,portrait
    - deploy-to-bitrise-io@2: {}

The practical QA choice is simple: use simulator Xcode tests for fast pull-request feedback and device testing for a smaller but meaningful set of release gates. Device testing is not a dumping ground for every flaky UI test. It is where you prove the build installs, launches, talks to system services, and survives device-specific behavior.

Test Reports as a Contract

The Test Reports add-on is not magic. The Bitrise docs describe three ways to get results into it: use dedicated testing Steps that already export results, add the Export test results to the Test reports Step for other runners, or export results from a custom Script Step. In all cases, the Deploy to Bitrise.io Step must run after the test results exist. The docs also note that Deploy to Bitrise.io must be version 1.4.1 or newer for Test Reports, and recommend 1.5.0 or newer. Pinning deploy-to-bitrise-io@2 avoids that old-version trap.

Source of resultsExport mechanismCommon failureDiagnosis
Android Unit TestStep exports expected Gradle result pathsTests fail but Tests tab is emptyCheck whether Deploy to Bitrise.io ran after the test Step
Xcode Test for iOSStep generates .xcresult and deploy handles itHTML report missing detailsConfirm the official Xcode Step produced an .xcresult
Custom Gradle or shell commandcustom-test-results-exportXML files found locally but not in reportsVerify base_path and search_pattern match actual files
Device test StepDevice Step exports logs and reportsDevice artifacts exist but no grouped casesConfirm supported test type and result format

For AI coding agents, make the report contract explicit in the prompt or skill instructions. Tell the agent to preserve the Step order, use deploy-to-bitrise-io at the end, and keep XML export paths tied to the module being tested. Ready-made QA skills install from qaskills.sh with the qaskills CLI, but the underlying rule is the same: an agent should change the test command and the report export together, not one without the other.

Parallelism and Sharding Without Losing Signal

Bitrise supports test sharding through Pipelines and Workflow parallelism. The docs describe Bitrise-provided sharding calculation for iOS with the Xcode test shard calculation Step and for Android with Gradle Runner, plus Pipeline configuration that runs multiple copies of a testing Workflow. For iOS, Bitrise examples build a test bundle once, share BITRISE_TEST_SHARDS_PATH and BITRISE_TEST_BUNDLE_PATH through Deploy to Bitrise.io, then use xcode-test-without-building in downstream Workflows. For Android, the current supported use case page describes a Pipeline stage with unit_tests and ui_tests running in parallel.

Use sharding after the suite has stable reporting and deterministic setup. Sharding a flaky suite makes it faster to get random answers. A good adoption sequence is: separate unit and UI Workflows, publish reports, remove environment coupling, then shard.

pipelines:
  android_pr_checks:
    workflows:
      android_unit_tests: {}
      android_virtual_device_tests: {}

workflows:
  android_unit_tests:
    steps:
    - git-clone: {}
    - android-unit-test@1:
        inputs:
        - project_location: ${PROJECT_LOCATION}
        - module: app
        - variant: debug
    - deploy-to-bitrise-io@2: {}

  android_virtual_device_tests:
    steps:
    - git-clone: {}
    - android-build-for-ui-testing@0:
        inputs:
        - project_location: ${BITRISE_SOURCE_DIR}
        - module: app
        - variant: debug
    - virtual-device-testing-for-android@1:
        inputs:
        - test_type: instrumentation
    - deploy-to-bitrise-io@2: {}

That Pipeline does not depend on build artifact sharing, so both Workflows clone and work independently. It is wasteful for very large builds but excellent for clarity. Once the UI build cost dominates, switch to a build Workflow that publishes the APK and test APK as intermediate artifacts, then test Workflows consume them.

Caching and Dependency Setup

Bitrise has official dependency and caching guidance, and the best testing setup follows the project type. Android Steps run Gradle tasks in a CI environment and can handle dependencies if Gradle is configured correctly. iOS workflows often need CocoaPods, Carthage, Swift Package Manager, or npm dependency Steps before Xcode Steps. Caching helps, but it should not hide missing dependency declarations.

EcosystemCache targetWhat to avoid
Android GradleGradle dependency and build cachesCaching generated test reports or APK outputs as if they were dependencies
iOS CocoaPodsPods and spec repos when appropriateCaching a workspace while the Podfile.lock changed
React Native mobileNode package cache plus native dependency cachesLetting JS install failures appear later as Xcode or Gradle failures
FlutterFlutter package cacheAdding extra install Steps when official Flutter Steps already handle packages

Keep cache pull near the top and cache push after the test or build Steps that should warm the cache. Do not push cache after a failed dependency installation unless the Step explicitly handles that safely. If a build becomes flaky only after cache is enabled, run one diagnostic build with cache pull disabled. That is faster than rewriting tests that were never the problem.

A Real Failure Mode: Empty Tests Tab After Green Tests

Suppose Android unit tests pass, Gradle prints a normal test summary, the build is green, but the Bitrise Tests tab says there are no test results. Diagnose it in this order.

CheckWhat to inspectLikely fix
Deploy Step orderIs deploy-to-bitrise-io after the test Step?Move it to the end of the Workflow
Deploy Step versionIs the Step older than 1.4.1?Pin deploy-to-bitrise-io@2
Custom report pathDid the test runner write XML outside the default path?Add custom-test-results-export with the right base path
Module nameDoes the path include app while tests run in another module?Parameterize the module and base path
Step choiceDid a shell Step run tests instead of an official test Step?Export results explicitly before deploy

The recurring root cause is that teams think "artifact uploaded" and "test report parsed" are the same thing. They are not. Artifacts let you download files. Test Reports need files in a supported format and a Deploy Step that knows where to send them. If your script writes JUnit XML, export it intentionally.

workflows:
  custom_gradle_test_reports:
    steps:
    - git-clone: {}
    - script@1:
        title: Run a custom Gradle verification task
        inputs:
        - content: |-
            #!/usr/bin/env bash
            set -euo pipefail
            ./gradlew :feature-login:testDebugUnitTest
    - custom-test-results-export@1:
        inputs:
        - test_name: feature-login unit tests
        - base_path: ${BITRISE_SOURCE_DIR}/feature-login/build/test-results
        - search_pattern: '*.xml'
    - deploy-to-bitrise-io@2: {}

Agent-Friendly Guardrails

AI coding agents are useful in Bitrise work because configuration changes are text, failures are log-heavy, and most fixes are repeatable. The risk is that agents overfit to one failure and damage the mobile build contract. Give them constraints that match the system.

Use this agent checklist before accepting a Bitrise testing PR:

GuardrailWhy it matters
Preserve official Step IDs unless replacing the whole strategyStep outputs feed later Steps
Keep report export adjacent to the test commandReports fail silently when paths drift
Do not mix simulator, emulator, and device goals in one nameReviewers need to know what failed
Pin major Step versions in examplesLatest can change behavior without a code diff
Treat secrets as Bitrise Secrets, not YAML valuesLogs and repository history are permanent

Ask the agent to produce one Workflow per validation goal, plus a short failure table. That gets better results than "make CI faster" because it forces the model to name the tradeoff.

Frequently Asked Questions

Is Bitrise testing only for mobile apps?

Bitrise is strongest for mobile apps because its stacks, project scanner, code signing, Android, and Xcode Steps are designed around mobile CI. You can run general scripts, web tests, and custom tools in Bitrise, but the platform's advantage is clearest when a build needs Android SDKs, Xcode, signing assets, simulator outputs, APKs, IPAs, and mobile test artifacts. For a web-only QA suite, GitHub Actions or another general CI may be simpler.

Should Android instrumentation tests run on local emulators or Firebase virtual devices?

Use local emulators when you need tight log access, direct adb behavior, and a smaller debug loop inside one Workflow. Use Bitrise Virtual Device Testing for Android when you want managed device execution and easier matrix-style confidence. Both paths start with android-build-for-ui-testing, but they diverge after artifact creation. Most teams should keep fast unit tests separate, then run a smaller instrumentation suite on one of these paths for pull requests or release candidates.

Why are my Bitrise Test Reports empty when the build passed?

A passing command does not automatically create a parsed report. Confirm that your test runner generated XML or .xcresult files, that the relevant official test Step exported them, or that custom-test-results-export points to the correct directory. Then confirm deploy-to-bitrise-io runs after that export and uses a modern major version. The most common bug is putting Deploy too early or changing a module path without updating the report export.

How should I introduce sharding without making failures harder to debug?

First separate unit, UI, and device Workflows. Second, publish stable reports from each one. Third, remove shared mutable state, including account reuse, cache-dependent fixtures, and order-dependent tests. Only then shard. Start with a small shard count and name each Workflow or Pipeline target clearly. If failures become harder to triage, keep the sharded path for scheduled builds and preserve a non-sharded pull-request Workflow for diagnosis.