コンテンツへスキップ

Xcodeテストのシャーディング

要件
  • Xcodeテストインサイトが設定されていること。シャーディングは過去30日間のテストインサイトのタイミングデータを使用してシャードを均等に配分します。インサイトがない場合、すべてのシャードはテストスイートが不明であるかのように実行されます。

Xcodeプロジェクト向けのテストシャーディングでは、tuist xcodebuild build-for-testingを使用してシャードプランを作成し、tuist xcodebuild testを使用して各シャードを実行します。

仕組み#

テストシャーディングは2段階のワークフローに従います。

  1. ビルドフェーズ: Tuistはビルドされた.xctestrunファイルからテストモジュールを読み取り、サーバー上でシャードプランを作成します。サーバーは過去30日間の履歴テストタイミングデータを使用して、各シャードの実行時間がほぼ同じになるようにテストをシャード間で配分します。ビルドフェーズは、CIシステムが並列ランナーを起動するために使用するシャードマトリックスを出力します。
  2. テストフェーズ: 各継続的インテグレーションランナーはシャードインデックスと正確なシャードプラン識別子を受け取り、そのプランのテスト選択を実行します。

スイート粒度の場合、サーバーはモジュールごとに既知のスイートを選択します。現在の.xctestrun内の各モジュールについて、ビルド対象ブランチでの直近の数回のCI実行におけるスイートを統合し、ブランチ履歴のないモジュールについてはプロジェクトのデフォルトブランチにフォールバックします。最新の実行だけでなく複数の実行を読み取ることが重要なのは、シャーディングされた実行では各シャードの完了時にシャードごとのテスト実行がアップロードされるため、最新の実行にはこれまでに取り込まれたモジュールの一部しか含まれないこと、また、最新のブランチ実行でスキップされたスイートが選択的テスト実行によって隠蔽されないようにするためです。

--shard-granularity suiteを使用すると、Tuistは要求されたシャード数に合わせて既知のスイートを配分し、最後のシャードをキャッチオールとして使用します。例えば、--shard-total 5はシャードインデックス0から4を生成し、シャードインデックス4がキャッチオールとなります。通常のシャードは、割り当てられたスイートに対して-only-testingを指定して実行されます。最後のシャードは-only-testingなしで実行され、以前のシャードに割り当てられたすべてのスイートに対して-skip-testingを渡すため、計画されたスイートに加えて、新たに追加されたスイートや履歴在庫に欠落しているスイートも実行されます。モジュール粒度では.xctestrunファイルがモジュールリストを提供するため、キャッチオールは不要です。

ビルドフェーズ#

テストをビルドし、シャードプランを作成します。

bash
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5

このコマンドは以下の処理を行います。

  1. xcodebuild build-for-testingでテストをビルドします
  2. 履歴タイミングデータを使用してTuistサーバー上にシャードプランを作成します
  3. .xctestproductsバンドルをアップロードするか、シャードランナーが使用するシャードアーカイブを書き出します
  4. CIシステム用のシャードマトリックスを出力します

ビルドオプション#

フラグ環境変数説明
--shard-max <N>TUIST_TEST_SHARD_MAXシャードの最大数。--shard-max-durationと共に使用してシャード数を制限します
--shard-min <N>TUIST_TEST_SHARD_MINシャードの最小数
--shard-total <N>TUIST_TEST_SHARD_TOTALシャードの正確な数(--shard-min/--shard-maxと排他)。スイート粒度の場合、最後のシャードがキャッチオールとなります
--shard-max-duration <MS>TUIST_TEST_SHARD_MAX_DURATIONシャードあたりの目標最大期間(ミリ秒)
--shard-granularity <LEVEL>TUIST_TEST_SHARD_GRANULARITYmodule(デフォルト)はテストモジュール全体をシャード間で配分します。suiteはより細かなバランス調整のために個別のテストクラスを配分します
--shard-reference <REF>TUIST_SHARD_REFERENCEシャードプランの一意の識別子(サポートされているCIプロバイダーで自動導出)
--shard-archive-path <PATH>TUIST_TEST_SHARD_ARCHIVE_PATHTuistが最適化されたシャードアーカイブを書き込むパス。テスト製品をリモートストレージにアップロードする代わりに使用されます

テストフェーズ#

各シャードランナーは割り当てられたテストを実行します。

bash
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'

テストオプション#

フラグ環境変数説明
--shard-index <N>TUIST_SHARD_INDEX実行するシャードのゼロベースインデックス
--shard-plan-id <IDENTIFIER>TUIST_SHARD_PLAN_IDビルドフェーズによって出力される正確なシャードプラン識別子。生成されたプロバイダー出力にはこの値が自動的に含まれます
--shard-reference <REF>TUIST_SHARD_REFERENCEシャードプランの一意の識別子(サポートされているCIプロバイダーで自動導出)
--shard-archive-path <PATH>TUIST_TEST_SHARD_ARCHIVE_PATHローカルで管理されるシャードアーカイブへのパス。Tuistはリモートストレージからテスト製品をダウンロードする代わりにこれを展開します

Tuistは.xctestproductsバンドルをダウンロードし、そのシャードに割り当てられたテストのみを含むようにフィルタリングします。

継続的インテグレーション#

Tuistは以下のCIプロバイダーを自動的に検出します。

その他のプロバイダーの場合は、.tuist-shard-matrix.jsonファイルを参照して並列ジョブを設定してください。

GitHub Actions#

マトリックス戦略を使用してシャードを並列に実行します。

yaml
name: Tests
on: [pull_request]
jobs:
build:
name: Build test shards
runs-on: macos-latest
outputs:
matrix: ${{ steps.build.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- run: tuist auth login
- id: build
run: |
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
test:
name: "Shard #${{ matrix.shard }}"
needs: build
runs-on: macos-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.build.outputs.matrix) }}
env:
TUIST_SHARD_INDEX: ${{ matrix.shard }}
TUIST_SHARD_PLAN_ID: ${{ matrix.shard_plan_id }}
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- run: tuist auth login
- run: |
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'

GitLab CI#

Tuistは.tuist-shard-child-pipeline.ymlを生成し、これを子パイプラインとしてトリガーできます。生成されたシャードジョブが拡張する.tuist-shardテンプレートジョブを定義します。

yaml
# .gitlab-ci.yml
stages:
- build
- test
build-shards:
stage: build
tags: [macos]
script:
- tuist auth login
- |
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
artifacts:
paths:
- .tuist-shard-child-pipeline.yml
test-shards:
stage: test
needs: [build-shards]
trigger:
include:
- artifact: .tuist-shard-child-pipeline.yml
job: build-shards
strategy: depend
yaml
# .gitlab/shard-template.yml
.tuist-shard:
tags: [macos]
script:
- tuist auth login
- |
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'

CircleCI#

Tuistはcontinuation orb用のパラメーターを含む.tuist-shard-continuation.jsonを生成します。shard-plan-idパラメーターは継続設定で宣言する必要があります。

yaml
# .circleci/config.yml
version: 2.1
setup: true
orbs:
continuation: circleci/continuation@1
jobs:
build-shards:
macos:
xcode: "16.0"
steps:
- checkout
- run:
name: Build and plan shards
command: |
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.yml
parameters: .tuist-shard-continuation.json
workflows:
setup:
jobs:
- build-shards
yaml
# .circleci/continue-config.yml
version: 2.1
parameters:
shard-indices:
type: string
default: ""
shard-count:
type: integer
default: 0
shard-plan-id:
type: string
default: ""
jobs:
test-shard:
macos:
xcode: "16.0"
parameters:
shard-index:
type: integer
steps:
- checkout
- run:
name: Run shard
command: |
export TUIST_SHARD_INDEX=<< parameters.shard-index >>
export TUIST_SHARD_PLAN_ID=<< pipeline.parameters.shard-plan-id >>
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はシャードごとに1つのステップを含む.tuist-shard-pipeline.ymlを生成します。buildkite-agent pipeline uploadでアップロードしてください。

yaml
# pipeline.yml
steps:
- 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.yml
agents:
queue: macos

生成された各ステップには環境にTUIST_SHARD_INDEXとTUIST_SHARD_PLAN_IDが設定されます。共有スクリプトを使用して、各シャードステップにテストコマンドを追加します。

bash
# .buildkite/shard-step.sh
#!/bin/bash
tuist auth login
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'

Codemagic#

Codemagicは動的なマトリックスジョブをサポートしていないため、シャードごとに個別のワークフローを定義します。Tuistはビルドワークフロー内で使用するためにTUIST_SHARD_MATRIXとTUIST_SHARD_COUNTをCM_ENVファイルに書き込みます。CodemagicのCM_ENVはワークフロースコープであるため、正確なシャードプラン識別子はワークフローの境界を越えません。テストワークフローは参照ベースのシャード検索にフォールバックします。

yaml
# codemagic.yaml
workflows:
build-shards:
name: Build test shards
instance_type: mac_mini_m2
environment:
xcode: latest
scripts:
- name: Build and plan shards
script: |
tuist auth login
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
test-shard-0: &shard-workflow
name: "Shard #0"
instance_type: mac_mini_m2
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 0
scripts:
- name: Run shard
script: |
tuist auth login
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'
test-shard-1:
<<: *shard-workflow
name: "Shard #1"
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 1
test-shard-2:
<<: *shard-workflow
name: "Shard #2"
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 2
test-shard-3:
<<: *shard-workflow
name: "Shard #3"
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 3
test-shard-4:
<<: *shard-workflow
name: "Shard #4"
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 4

Bitrise#

Bitriseでは、Tuistは(shard_plan_idを含む).tuist-shard-matrix.jsonをBITRISE_DEPLOY_DIRに書き込み、下流のパイプラインステージでビルド成果物として利用できるようにします。各テストワークフローは、ダウンロードした成果物からenvmanを使用してプランIDを抽出し、テストコマンドの環境に渡します。

yaml
# bitrise.yml
pipelines:
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 shards
inputs:
- content: |
tuist auth login
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
- deploy-to-bitrise-io: {}
test-shard-0: &shard-workflow
envs:
- TUIST_SHARD_INDEX: 0
steps:
- pull-intermediate-files: {}
- script:
title: Set shard plan ID from artifact
inputs:
- content: |
PLAN_ID=$(jq -r '.shard_plan_id' "$BITRISE_DEPLOY_DIR/.tuist-shard-matrix.json")
envman add --key TUIST_SHARD_PLAN_ID --value "$PLAN_ID"
- script:
title: Run shard
inputs:
- content: |
tuist auth login
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'
test-shard-1:
<<: *shard-workflow
envs:
- TUIST_SHARD_INDEX: 1
test-shard-2:
<<: *shard-workflow
envs:
- TUIST_SHARD_INDEX: 2
test-shard-3:
<<: *shard-workflow
envs:
- TUIST_SHARD_INDEX: 3
test-shard-4:
<<: *shard-workflow
envs:
- TUIST_SHARD_INDEX: 4
Tip

Bitriseはランタイムでの動的な並列ジョブ作成をサポートしていません。パイプラインステージで固定数のシャードワークフローを定義してください。ステージ内のワークフローは自動的に並列で実行されます。

共有ボリューム#

デフォルトでは、ビルドフェーズが.xctestproductsバンドルをリモートストレージにアップロードし、各シャードランナーがそれをダウンロードします。CIプロバイダーが共有ボリューム(ジョブ間でマウントされる永続ストレージ)をサポートしている場合、テスト製品を共有ファイルシステム経由で渡すことで、このアップロード/ダウンロードを完全にスキップできます。

これにより、特に大規模なテストバンドルの場合、シャードの起動時間を大幅に短縮できます。

共有ボリュームを使用するには:

  1. ビルドフェーズで、共有ボリュームを指す-testProductsPathを渡し、リモートアップロードをスキップするために--shard-skip-uploadを追加します。
bash
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5 \
--shard-skip-upload \
-testProductsPath /path/to/shared/volume/$UNIQUE_ID/MyScheme.xctestproducts
  1. テストフェーズで、同じ-testProductsPathを渡し、Tuistがテスト製品をダウンロードする代わりにローカルで読み取るようにします。
bash
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-testProductsPath /path/to/shared/volume/$UNIQUE_ID/MyScheme.xctestproducts
Important

同時実行間の競合を避けるため、ワークフロー実行ごとに一意のパス(例:CI実行IDを含める)を使用してください。また、シャード処理完了後にテスト製品をクリーンアップし、ボリューム上に古いデータが蓄積しないようにしてください。

フラグ環境変数説明
--shard-skip-uploadTUIST_TEST_SHARD_SKIP_UPLOADテスト製品バンドルのリモートストレージへのアップロードをスキップ

自己管理の成果物#

CIプロバイダーにすでに成果物のアップロードおよびダウンロードステップがある場合、アーカイブと展開をTuistに任せ、転送をCIに任せることができます。

  1. ビルドフェーズで、--shard-archive-pathを渡し、Tuistがテスト製品をアップロードする代わりに最適化されたシャードアーカイブをローカルに書き込むようにします。
bash
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5 \
--shard-archive-path /tmp/shards/${UNIQUE_ID}/bundle.aar
  1. CIのネイティブな成果物ステップを使用してそのアーカイブをアップロードします。

  2. 各テストフェーズジョブで、アーカイブをダウンロードし、同じパスをTuistに渡します。

bash
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-archive-path /tmp/shards/${UNIQUE_ID}/bundle.aar

--shard-archive-pathが設定されている場合、Tuistはリモートのテスト製品転送をスキップし、代わりにローカルアーカイブを使用します。--shard-skip-uploadも渡した場合、アーカイブパスが優先されます。

Namespace#

NamespaceランナーはGitHub Actionsの成果物と相性が良いです。TUIST_TEST_SHARD_ARCHIVE_PATHを一度設定し、ビルドジョブでシャードアーカイブをローカルに書き込み、アップロードして、各シャードジョブでtuist xcodebuild testを実行する前にダウンロードするようにします。

yaml
name: Tests
on: [pull_request]
env:
TUIST_TEST_SHARD_ARCHIVE_PATH: /tmp/shards/${{ github.run_id }}/bundle.aar
jobs:
build:
name: Build test shards
runs-on: namespace-profile-default-macos
outputs:
matrix: ${{ steps.build.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- run: tuist auth login
- id: build
run: |
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
- uses: actions/upload-artifact@v4
with:
name: test-shard-archive
path: ${{ env.TUIST_TEST_SHARD_ARCHIVE_PATH }}
- if: always()
run: rm -rf /tmp/shards/${{ github.run_id }}
test:
name: "Shard #${{ matrix.shard }}"
needs: build
runs-on: namespace-profile-default-macos
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.build.outputs.matrix) }}
env:
TUIST_SHARD_INDEX: ${{ matrix.shard }}
TUIST_SHARD_PLAN_ID: ${{ matrix.shard_plan_id }}
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- run: tuist auth login
- uses: actions/download-artifact@v4
with:
name: test-shard-archive
path: /tmp/shards/${{ github.run_id }}
- run: |
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'
- if: always()
run: rm -rf /tmp/shards/${{ github.run_id }}