Parallel Execution
This guide covers running Tapsmith tests in parallel across multiple devices and splitting test suites across CI machines with sharding.
Parallel Execution
Section titled “Parallel Execution”Overview
Section titled “Overview”By default, Tapsmith runs tests sequentially on a single device. To run tests in parallel, set the workers option in your config file or use the --workers / -j CLI flag:
npx tapsmith test --workers 4npx tapsmith test -j 4Or in your config:
import { defineConfig } from "tapsmith";
export default defineConfig({ workers: 4,});Each worker gets its own device, daemon instance, and agent. Tapsmith distributes test files across workers using a work-stealing queue — workers pull the next available file when they finish their current one, which provides natural load balancing without requiring upfront knowledge of test durations.
Android Parallel Setup
Section titled “Android Parallel Setup”The recommended approach for Android is to let Tapsmith launch emulator instances automatically. Set launchEmulators: true and specify the avd to use:
import { defineConfig } from "tapsmith";
export default defineConfig({ apk: "./app-debug.apk", package: "com.example.myapp", workers: 4, launchEmulators: true, avd: "Pixel_9_API_35",});With this configuration, Tapsmith launches repeated read-only instances of the specified AVD — one per worker. The AVD must already exist on your system (avdmanager list avd to check). Read-only instances share the same base snapshot, so they boot quickly and do not interfere with each other.
If you have devices or emulators already running and want Tapsmith to use them before launching new ones, set deviceStrategy: "prefer-connected":
export default defineConfig({ apk: "./app-debug.apk", package: "com.example.myapp", workers: 4, launchEmulators: true, avd: "Pixel_9_API_35", deviceStrategy: "prefer-connected",});With prefer-connected, Tapsmith assigns already-running healthy devices first and only launches additional emulators to fill the remaining worker slots.
iOS Parallel Setup
Section titled “iOS Parallel Setup”For iOS, Tapsmith clones simulators automatically when multiple workers are requested. Specify the base simulator name and Tapsmith handles the rest:
import { defineConfig } from "tapsmith";
export default defineConfig({ app: "./build/MyApp.app", package: "com.example.myapp", workers: 4, simulator: "iPhone 17",});Tapsmith creates clones of the specified simulator device type for each worker beyond the first. These clones are managed automatically — they are created at the start of the run and cleaned up afterward.
No additional setup is required beyond having Xcode installed with the target simulator runtime available.
Worker Output
Section titled “Worker Output”When running with multiple workers, console reporters prefix each line of output with the worker index so you can tell which worker produced which result:
[worker 0] PASS login flow > successful login (2104ms) [worker 1] PASS settings > can toggle notifications (1842ms) [worker 0] PASS login flow > invalid credentials (1531ms) [worker 2] FAIL profile > can update avatar (30012ms) [worker 1] PASS settings > can change language (2210ms)Each TestResult object includes a workerIndex field, which is available to custom reporters and in the JSON/JUnit output. This is useful for correlating failures with specific devices when debugging.
Test Independence
Section titled “Test Independence”Tests running in parallel must be independent of each other. Each worker runs in its own process with its own device, app instance, and agent — there is no shared state between workers at the Tapsmith level.
Guidelines for writing parallel-safe tests:
- No shared mutable state. Do not rely on one test setting up data that another test reads. Each test should create whatever it needs.
- Use unique test data. If tests create accounts, use unique emails or usernames per test to avoid collisions on shared backends.
- Avoid assumptions about execution order. Test files are distributed dynamically across workers. A file that ran first in sequential mode may run last in parallel mode.
- Be careful with backend state. Each worker has its own isolated app, but if your tests modify shared server-side data (database records, feature flags, etc.), tests can interfere with each other. Use test-scoped data or per-test API keys where possible.
Projects (Multi-Device Targeting)
Section titled “Projects (Multi-Device Targeting)”Overview
Section titled “Overview”Tapsmith supports Playwright-style project configuration for targeting multiple devices or platforms in a single test run. Each project defines its own device settings via a use block, and Tapsmith provisions separate devices for each project.
import { defineConfig } from "tapsmith";
export default defineConfig({ package: "com.example.app", timeout: 30_000,
projects: [ { name: "Pixel 6", use: { platform: "android", avd: "Pixel_6_API_34", apk: "./android/app-debug.apk", launchEmulators: true, }, }, { name: "iPhone 16", use: { platform: "ios", simulator: "iPhone 16", app: "./ios/MyApp.app", }, }, ],});Top-level fields (package, timeout, etc.) are inherited by every project as defaults. The use block in each project overrides these defaults with device-specific settings.
A single project must not mix Android fields (avd, apk) with iOS fields (simulator, app). Tapsmith validates this at startup and reports an error if it finds a conflict.
Per-Project Workers
Section titled “Per-Project Workers”Each project can set its own workers count. Per-project worker counts are additive — they do not consume from a shared global budget:
export default defineConfig({ projects: [ { name: "android", workers: 2, use: { platform: "android", avd: "Pixel_6_API_34", apk: "./android.apk", launchEmulators: true, }, }, { name: "ios", workers: 1, use: { platform: "ios", simulator: "iPhone 16", app: "./ios/MyApp.app", }, }, ],});This runs the Android project on 2 devices and the iOS project on 1 device, concurrently. Total parallelism is 3 workers.
When per-project workers are not set, the global workers budget is split proportionally across projects based on their test file count, with at least 1 worker per project.
If the total worker count comes out to 1 (for example, workers: 1 globally with no per-project overrides), Tapsmith runs projects sequentially, tearing down and re-provisioning the device between each. This is useful on machines that can only support one emulator or simulator at a time.
Cross-Platform Testing
Section titled “Cross-Platform Testing”The most common use of projects is running the same test suite against both Android and iOS:
import { defineConfig } from "tapsmith";
export default defineConfig({ projects: [ { name: "android", use: { platform: "android", avd: "Pixel_6_API_34", apk: "./android.apk", launchEmulators: true, }, workers: 2, }, { name: "ios", use: { platform: "ios", simulator: "iPhone 16", app: "./ios/MyApp.app", }, workers: 1, }, ],});A single npx tapsmith test invocation runs both platforms. Test files are shared across projects by default (controlled by testMatch), or you can use per-project testMatch to run different files on each platform.
Projects also work with --ui and --watch modes. UI mode groups tests by project name, and watch mode re-runs only the affected project when you edit a test file.
Setup Projects
Section titled “Setup Projects”Use the dependencies field to run setup tasks before the main test suite. This is useful for authentication flows — run login once, save the app state, and reuse it across all test workers:
import { defineConfig } from "tapsmith";
export default defineConfig({ projects: [ { name: "setup", testMatch: ["**/auth.setup.ts"], }, { name: "tests", dependencies: ["setup"], use: { appState: "./auth-state.tar.gz", }, }, ],});The setup project runs first. When it completes, the tests project starts with the saved appState pre-loaded. This avoids repeating login flows in every test and reduces overall suite time.
CI Sharding
Section titled “CI Sharding”Splitting Across Machines
Section titled “Splitting Across Machines”When your test suite is too large for a single CI machine, use sharding to split it across multiple jobs. The --shard=x/y flag deterministically partitions test files:
# Machine 1: runs roughly the first quarter of test filesnpx tapsmith test --shard=1/4
# Machine 2: second quarternpx tapsmith test --shard=2/4
# Machine 3: third quarternpx tapsmith test --shard=3/4
# Machine 4: last quarternpx tapsmith test --shard=4/4Sharding is file-based and deterministic — the same --shard value always selects the same set of files, regardless of worker count or execution order. This means re-running a failed shard reproduces the exact same file set.
GitHub Actions Matrix
Section titled “GitHub Actions Matrix”Here is a complete workflow that shards tests across 4 CI jobs, then merges the results:
name: Mobile Tests
on: push: branches: [main] pull_request: branches: [main]
jobs: test: runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: shard: [1, 2, 3, 4]
steps: - uses: actions/checkout@v4
- name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "22" cache: "npm"
- name: Install dependencies run: npm ci
- name: Set up JDK uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "17"
- name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm
- name: Start emulator and run tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: 35 arch: x86_64 emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim script: npx tapsmith test --shard=${{ matrix.shard }}/4
- name: Upload blob report if: always() uses: actions/upload-artifact@v4 with: name: blob-report-${{ matrix.shard }} path: blob-report/
- name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 with: name: tapsmith-results-${{ matrix.shard }} path: tapsmith-results/ retention-days: 14
merge-reports: needs: test runs-on: ubuntu-latest if: always() steps: - uses: actions/checkout@v4
- name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "22" cache: "npm"
- name: Install dependencies run: npm ci
- name: Download blob reports uses: actions/download-artifact@v4 with: pattern: blob-report-* path: all-blob-reports merge-multiple: true
- name: Merge reports run: npx tapsmith merge-reports all-blob-reports
- name: Upload HTML report uses: actions/upload-artifact@v4 with: name: tapsmith-report path: tapsmith-report/Set fail-fast: false so that a failure in one shard does not cancel the other shards. This ensures you get complete results even when some tests fail.
Merging Reports
Section titled “Merging Reports”When --shard is used, Tapsmith automatically adds the blob reporter alongside your configured reporters. Each shard writes its results to a blob-report/ directory.
After all shards complete, collect the blob reports and merge them into a single report:
# Merge blob reports from all shardsnpx tapsmith merge-reports ./all-blob-reports
# Open the merged HTML reportnpx tapsmith show-reportThe merge-reports command reads all blob files from the specified directory and produces a unified HTML report in tapsmith-report/. This report contains results from every shard, ordered and grouped as if the suite had run on a single machine.
Combining Workers and Sharding
Section titled “Combining Workers and Sharding”Each shard can still use multiple workers internally. The total parallelism is shards x workers:
# 4 CI machines, each running 2 workers = 8 devices totalnpx tapsmith test --shard=1/4 --workers 2This is useful when your CI machines have enough resources to run multiple emulators but you still want to split the suite across machines for faster wall-clock time.
For the config file equivalent:
import { defineConfig } from "tapsmith";
export default defineConfig({ apk: "./app-debug.apk", package: "com.example.myapp", workers: 2, launchEmulators: true, avd: "Pixel_9_API_35", // shard is typically set via CLI, not config});Then run with --shard=x/y from each CI job.
Best Practices
Section titled “Best Practices”Choosing Worker Count
Section titled “Choosing Worker Count”Each device consumes significant system resources. Recommended starting points:
| Platform | RAM per device | CPU recommendation | Suggested workers |
|---|---|---|---|
| Android emulator | ~2 GB | 2 cores per emulator | 2—4 on a 16 GB machine |
| iOS simulator | ~1 GB | 1—2 cores per simulator | 2—4 on a 16 GB machine |
Start with 2 workers and increase gradually while monitoring system load. Too many workers on an under-resourced machine leads to slower tests (due to CPU/memory contention) and flaky timeouts. On CI, check what your runner provides — GitHub-hosted ubuntu-latest runners have 4 vCPUs and 16 GB RAM, which can comfortably support 2 Android emulators.
For CI, consider increasing the default timeout to account for the overhead of multiple emulators competing for resources:
const isCI = process.env.CI === "true"
export default defineConfig({ workers: isCI ? 2 : 4, timeout: isCI ? 60_000 : 30_000,});Device Isolation
Section titled “Device Isolation”Each worker gets its own device with its own app installation. App data is fully isolated — actions in one worker do not affect another worker’s app state.
However, device isolation does not extend to your backend. If multiple workers modify the same server-side resources (user accounts, database rows, shared queues), tests can interfere with each other. Strategies to handle this:
- Test-scoped data. Generate unique usernames, email addresses, or IDs per test.
- Per-worker backend environments. If your backend supports it, point each worker at a separate test tenant or namespace.
- Idempotent tests. Design tests so they succeed regardless of pre-existing state.
Reporter Considerations
Section titled “Reporter Considerations”When running in parallel or with sharding, keep these reporter behaviors in mind:
- Worker index. Every
TestResultincludesworkerIndex, available in JSON, JUnit, and custom reporters. Use it to correlate failures with specific devices. - Blob reporter. Automatically added when
--shardis used. Writes machine-readable results toblob-report/for later merging. - HTML reporter. Works well with both parallel and sharded runs. After merging, the HTML report shows all results in a single view.
- Console reporters. In parallel mode, output is prefixed with
[worker N]to distinguish which worker produced each result.
For CI pipelines with sharding, a typical reporter setup is:
export default defineConfig({ reporter: [ ["html", { outputFolder: "tapsmith-report" }], ["junit", { outputFile: "tapsmith-results/junit.xml" }], ],});The blob reporter is added automatically when --shard is used, so you do not need to configure it manually.
Reference
Section titled “Reference”- Configuration — full list of config options including
workers,shard,launchEmulators,avd, andsimulator. - CI Setup — complete CI workflow examples for Android and iOS.
- API Reference —
TestResultfields, reporter APIs, and device methods.