Xcode test sharding
- A Tuist account and project
- Test Insights configured (for optimal shard balancing)
Test sharding for Xcode projects uses tuist xcodebuild build-for-testing to create a shard plan and tuist xcodebuild test to execute each shard.
How it works#
Test sharding follows a two-phase workflow:
- Build phase: Tuist enumerates your tests and creates a shard plan on the server. The server uses historical test timing data from the last 30 days to distribute tests across shards so each shard takes roughly the same amount of time. The build phase outputs a shard matrix that your CI system uses to spawn parallel runners.
- Test phase: Each CI runner receives a shard index and executes only the tests assigned to that shard.
Build phase#
Build your tests and create a shard plan:
tuistxcodebuildbuild-for-testing \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16' \
--shard-total5This command:
- Builds your tests with
xcodebuild build-for-testing - Creates a shard plan on the Tuist server using historical timing data
- Uploads the
.xctestproductsbundle or writes a shard archive for use by shard runners - Outputs a shard matrix for your CI system
Build options#
| Flag | Environment variable | Description |
|---|---|---|
--shard-max <N> | TUIST_TEST_SHARD_MAX | Maximum number of shards. Used with --shard-max-duration to cap the shard count |
--shard-min <N> | TUIST_TEST_SHARD_MIN | Minimum number of shards |
--shard-total <N> | TUIST_TEST_SHARD_TOTAL | Exact number of shards (mutually exclusive with --shard-min/--shard-max) |
--shard-max-duration <MS> | TUIST_TEST_SHARD_MAX_DURATION | Target maximum duration per shard in milliseconds |
--shard-granularity <LEVEL> | TUIST_TEST_SHARD_GRANULARITY | module (default) distributes entire test modules across shards; suite distributes individual test classes for finer-grained balancing |
--shard-reference <REF> | TUIST_SHARD_REFERENCE | Unique identifier for the shard plan (auto-derived on supported CI providers) |
--shard-archive-path <PATH> | TUIST_TEST_SHARD_ARCHIVE_PATH | Path where Tuist writes the optimized shard archive instead of uploading test products to remote storage |
Test phase#
Each shard runner executes its assigned tests:
tuistxcodebuildtest \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16'Test options#
| Flag | Environment variable | Description |
|---|---|---|
--shard-index <N> | TUIST_SHARD_INDEX | Zero-based index of the shard to execute |
--shard-reference <REF> | TUIST_SHARD_REFERENCE | Unique identifier for the shard plan (auto-derived on supported CI providers) |
--shard-archive-path <PATH> | TUIST_TEST_SHARD_ARCHIVE_PATH | Path to a locally managed shard archive; Tuist extracts it instead of downloading test products from remote storage |
Tuist downloads the .xctestproducts bundle and filters it to include only the tests assigned to that shard.
Continuous integration#
Tuist automatically detects the following CI providers:
For other providers, refer to the .tuist-shard-matrix.json file to set up parallel jobs.
GitHub Actions#
Use a matrix strategy to run shards in parallel:
name:Testson:[pull_request]jobs:build:name:Build test shardsruns-on:macos-latestoutputs:matrix:${{ steps.build.outputs.matrix }}steps:-uses:actions/checkout@v4-uses:jdx/mise-action@v2-run:tuistauthlogin-id:buildrun:|tuistxcodebuildbuild-for-testing \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16' \
--shard-total5test:name:"Shard #${{ matrix.shard }}"needs:buildruns-on:macos-lateststrategy:fail-fast:falsematrix:shard:${{ fromJson(needs.build.outputs.matrix).shard }}env:TUIST_SHARD_INDEX:${{ matrix.shard }}steps:-uses:actions/checkout@v4-uses:jdx/mise-action@v2-run:tuistauthlogin-run:|tuistxcodebuildtest \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16'GitLab CI#
Tuist generates a .tuist-shard-child-pipeline.yml that you trigger as a child pipeline. Define a .tuist-shard template job that the generated shard jobs extend:
# .gitlab-ci.ymlstages:-build-testbuild-shards:stage:buildtags:[macos]script:-tuistauthlogin-|tuistxcodebuildbuild-for-testing \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16' \
--shard-total5artifacts:paths:-.tuist-shard-child-pipeline.ymltest-shards:stage:testneeds:[build-shards]trigger:include:-artifact:.tuist-shard-child-pipeline.ymljob:build-shardsstrategy:depend# .gitlab/shard-template.yml.tuist-shard:tags:[macos]script:-tuistauthlogin-|tuistxcodebuildtest \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16'CircleCI#
Tuist generates a .tuist-shard-continuation.json with parameters for the continuation orb:
# .circleci/config.ymlversion:2.1setup:trueorbs:continuation:circleci/continuation@1jobs:build-shards:macos:xcode:"16.0"steps:-checkout-run:name:Build and plan shardscommand:| tuist auth login
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5-continuation/continue:configuration_path:.circleci/continue-config.ymlparameters:.tuist-shard-continuation.jsonworkflows:setup:jobs:-build-shards# .circleci/continue-config.ymlversion:2.1parameters:shard-indices:type:stringdefault:""shard-count:type:integerdefault:0jobs:test-shard:macos:xcode:"16.0"parameters:shard-index:type:integersteps:-checkout-run:name:Run shardcommand:| export TUIST_SHARD_INDEX=<< parameters.shard-index >>
tuist auth login
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'workflows:test:jobs:-test-shard:matrix:parameters:shard-index:[<< pipeline.parameters.shard-indices >>]Buildkite#
Tuist generates a .tuist-shard-pipeline.yml with one step per shard. Upload it with buildkite-agent pipeline upload:
# pipeline.ymlsteps:-label:"Build test shards"command:| tuist auth login
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
buildkite-agent pipeline upload .tuist-shard-pipeline.ymlagents:queue:macosEach generated step has TUIST_SHARD_INDEX set in its environment. Add the test command to each shard step using a shared script:
# .buildkite/shard-step.sh#!/bin/bashtuistauthlogintuistxcodebuildtest \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16'Codemagic#
Codemagic does not support dynamic matrix jobs, so define a separate workflow per shard. Tuist writes TUIST_SHARD_MATRIX and TUIST_SHARD_COUNT to the CM_ENV file for use within each workflow:
# codemagic.yamlworkflows:build-shards:name:Build test shardsinstance_type:mac_mini_m2environment:xcode:latestscripts:-name:Build and plan shardsscript:|tuistauthlogintuistxcodebuildbuild-for-testing \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16' \
--shard-total5test-shard-0:&shard-workflowname:"Shard #0"instance_type:mac_mini_m2environment:xcode:latestvars:TUIST_SHARD_INDEX:0scripts:-name:Run shardscript:|tuistauthlogintuistxcodebuildtest \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16'test-shard-1:<<:*shard-workflowname:"Shard #1"environment:xcode:latestvars:TUIST_SHARD_INDEX:1test-shard-2:<<:*shard-workflowname:"Shard #2"environment:xcode:latestvars:TUIST_SHARD_INDEX:2test-shard-3:<<:*shard-workflowname:"Shard #3"environment:xcode:latestvars:TUIST_SHARD_INDEX:3test-shard-4:<<:*shard-workflowname:"Shard #4"environment:xcode:latestvars:TUIST_SHARD_INDEX:4Bitrise#
On Bitrise, Tuist writes .tuist-shard-matrix.json to the BITRISE_DEPLOY_DIR, making it available as a build artifact for downstream pipeline stages. Use Bitrise Pipelines with pre-defined parallel workflows:
# bitrise.ymlpipelines:test-pipeline:stages:-build-stage:{}-test-stage:{}stages:build-stage:workflows:-build-shards:{}test-stage:workflows:-test-shard-0:{}-test-shard-1:{}-test-shard-2:{}-test-shard-3:{}-test-shard-4:{}workflows:build-shards:steps:-script:title:Build and plan shardsinputs:-content:| tuist auth login
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5test-shard-0:&shard-workflowenvs:-TUIST_SHARD_INDEX:0steps:-script:title:Run shardinputs:-content:| tuist auth login
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'test-shard-1:<<:*shard-workflowenvs:-TUIST_SHARD_INDEX:1test-shard-2:<<:*shard-workflowenvs:-TUIST_SHARD_INDEX:2test-shard-3:<<:*shard-workflowenvs:-TUIST_SHARD_INDEX:3test-shard-4:<<:*shard-workflowenvs:-TUIST_SHARD_INDEX:4Bitrise does not support dynamic parallel job creation at runtime. Define a fixed number of shard workflows in your pipeline stages — workflows within a stage run in parallel automatically.
Shared volumes#
By default, the build phase uploads the .xctestproducts bundle to remote storage, and each shard runner downloads it. If your CI provider supports shared volumes (persistent storage mounted across jobs), you can skip this upload/download entirely by passing the test products through a shared filesystem.
This can significantly reduce shard startup time, especially for large test bundles.
To use shared volumes:
- In the build phase, pass
-testProductsPathpointing to a shared volume and add--shard-skip-uploadto skip the remote upload:
tuistxcodebuildbuild-for-testing \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16' \
--shard-total5 \
--shard-skip-upload \
-testProductsPath/path/to/shared/volume/$UNIQUE_ID/MyScheme.xctestproducts- In the test phase, pass the same
-testProductsPathso Tuist reads the test products locally instead of downloading them:
tuistxcodebuildtest \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16' \
-testProductsPath/path/to/shared/volume/$UNIQUE_ID/MyScheme.xctestproductsUse a unique path per workflow run (e.g. include the CI run ID) to avoid collisions between concurrent runs. You should also clean up the test products after sharding completes to avoid accumulating stale data on the volume.
| Flag | Environment variable | Description |
|---|---|---|
--shard-skip-upload | TUIST_TEST_SHARD_SKIP_UPLOAD | Skip uploading the test products bundle to remote storage |
Self-managed artifacts#
If your CI provider already has artifact upload and download steps, you can let Tuist handle archive and extraction while your CI handles transport.
- In the build phase, pass
--shard-archive-pathso Tuist writes its optimized shard archive locally instead of uploading test products:
tuistxcodebuildbuild-for-testing \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16' \
--shard-total5 \
--shard-archive-path/tmp/shards/${UNIQUE_ID}/bundle.aarUpload that archive using your CI's native artifact step.
In each test phase job, download the archive and pass the same path back to Tuist:
tuistxcodebuildtest \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16' \
--shard-archive-path/tmp/shards/${UNIQUE_ID}/bundle.aarWhen --shard-archive-path is set, Tuist skips remote test-products transfer and uses the local archive instead. If you also pass --shard-skip-upload, the archive path takes precedence.
Namespace#
Namespace runners work well with GitHub Actions artifacts. Set TUIST_TEST_SHARD_ARCHIVE_PATH once so the build job writes the shard archive locally, upload it, and download it in each shard job before running tuist xcodebuild test:
name:Testson:[pull_request]env:TUIST_TEST_SHARD_ARCHIVE_PATH:/tmp/shards/${{ github.run_id }}/bundle.aarjobs:build:name:Build test shardsruns-on:namespace-profile-default-macosoutputs:matrix:${{ steps.build.outputs.matrix }}steps:-uses:actions/checkout@v4-uses:jdx/mise-action@v2-run:tuistauthlogin-id:buildrun:|tuistxcodebuildbuild-for-testing \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16' \
--shard-total5-uses:actions/upload-artifact@v4with:name:test-shard-archivepath:${{ env.TUIST_TEST_SHARD_ARCHIVE_PATH }}-if:always()run:rm-rf/tmp/shards/${{ github.run_id }}test:name:"Shard #${{ matrix.shard }}"needs:buildruns-on:namespace-profile-default-macosstrategy:fail-fast:falsematrix:shard:${{ fromJson(needs.build.outputs.matrix).shard }}env:TUIST_SHARD_INDEX:${{ matrix.shard }}steps:-uses:actions/checkout@v4-uses:jdx/mise-action@v2-run:tuistauthlogin-uses:actions/download-artifact@v4with:name:test-shard-archivepath:/tmp/shards/${{ github.run_id }}-run:|tuistxcodebuildtest \
-schemeMyScheme \
-destination'platform=iOS Simulator,name=iPhone 16'-if:always()run:rm-rf/tmp/shards/${{ github.run_id }}