콘텐츠로 이동

Xcode 테스트 샤딩

필수 조건
  • Xcode 테스트 인사이트가 구성되어야 합니다. 샤딩은 지난 30일간의 테스트 인사이트 타이밍 데이터를 사용하여 샤드를 균형 있게 분배하며, 인사이트가 없으면 모든 샤드는 해당 스위트가 알려지지 않은 상태로 실행됩니다.

Xcode 프로젝트의 테스트 샤딩은 tuist xcodebuild build-for-testing를 사용하여 샤드 계획을 생성하고 tuist xcodebuild test를 사용하여 각 샤드를 실행합니다.

작동 방식#

테스트 샤딩은 두 단계 워크플로를 따릅니다:

  1. 빌드 단계: Tuist는 빌드된 .xctestrun 파일에서 테스트 모듈을 읽고 서버에 샤드 계획을 생성합니다. 서버는 지난 30일간의 역사적 테스트 타이밍 데이터를 사용하여 각 샤드가 대략 동일한 시간이 소요되도록 테스트를 샤드에 분배합니다. 빌드 단계는 CI 시스템이 병렬 러너를 생성하는 데 사용하는 샤드 매트릭스를 출력합니다.
  2. 테스트 단계: 각 지속적 통합(CI) 러너는 샤드 인덱스와 정확한 샤드 계획 식별자를 받은 후, 해당 계획의 테스트 선택 항목을 실행합니다.

수트 단위의 세분성을 사용하면 서버는 모듈별로 알려진 수트를 선택합니다. 현재 .xctestrun의 각 모듈에 대해, 빌드 중인 브랜치에서 최근 몇 차례의 CI 실행에 포함된 수트를 합집합으로 취하며, 브랜치 이력이 없는 모듈의 경우 프로젝트의 기본 브랜치를 폴백으로 사용합니다. 최신 실행 하나만 읽지 않고 여러 실행을 읽는 것이 중요한 이유는, 샤딩된 실행은 각 샤드가 완료될 때마다 샤드당 하나의 테스트 실행을 업로드하므로 가장 최근 실행에는 지금까지 수집된 모듈의 일부만 포함되기 때문입니다. 또한 이렇게 하면 선택적 테스팅 실행에서 최신 브랜치 실행 시 건너뛴 수트가 숨겨지는 것을 방지할 수 있습니다.

--shard-granularity suite가 사용되면 Tuist는 요청된 샤드 수에 걸쳐 알려진 수트를 균형 있게 분배하고 마지막 샤드를 catch-all로 사용합니다. 예를 들어, --shard-total 5는 샤드 인덱스 0부터 4까지를 생성하며, 샤드 인덱스 4가 catch-all 역할을 합니다. 일반 샤드는 할당된 수트에 대해 -only-testing 옵션을 사용하여 실행됩니다. 마지막 샤드는 -only-testing 없이 실행되며 이전 샤드에 할당된 모든 수트에 대해 -skip-testing를 전달하므로, 계획된 수트 외에도 새로 추가된 수트나 역사적 목록에 누락된 수트를 실행합니다. 모듈 단위의 세분성은 .xctestrun 파일이 모듈 목록을 제공하므로 catch-all이 필요하지 않습니다.

빌드 단계#

테스트를 빌드하고 샤드 계획을 생성하세요:

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와 상호 배타적). 수트 단위의 세분성을 사용하면 마지막 샤드가 catch-all 역할을 합니다
--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실행할 샤드의 0 기반 인덱스입니다
--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 번들을 다운로드하고 해당 샤드에 할당된 테스트만 포함하도록 필터링합니다.

지속적 통합(CI)#

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는 샤드당 하나의 단계를 포함하는 .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는 빌드 워크플로 내에서 사용할 수 있도록 CM_ENV 파일에 TUIST_SHARD_MATRIX 및 TUIST_SHARD_COUNT를 기록합니다. 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가 BITRISE_DEPLOY_DIR에 .tuist-shard-matrix.json(shard_plan_id 포함)를 작성하여 다운스트림 파이프라인 단계에서 빌드 아티팩트로 사용할 수 있게 합니다. 각 테스트 워크플로는 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 xcodebuild test를 실행하기 전에 다운로드하도록 TUIST_TEST_SHARD_ARCHIVE_PATH를 한 번 설정하세요:

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 }}