跳至内容

Gradle 测试分片

Tuist Gradle 插件内置支持测试分片。它会根据 Tuist 在你项目的测试任务之前的 CI 运行中记录的测试套件制定计划,并利用 Tuist 服务器基于历史耗时数据创建均衡的分片计划。

要求

工作原理#

测试分片遵循两阶段工作流:

  1. 构建阶段: Tuist 在服务器上根据为你项目记录的测试套件创建分片计划。服务器使用过去 30 天的历史测试耗时数据,将测试分配到各个分片,使每个分片的耗时大致相同。构建阶段会输出一个分片矩阵,你的 CI 系统可用它来生成并行运行器。
  2. 测试阶段: 每个 CI 运行器接收一个分片索引,并仅执行分配给该分片的测试。最后一个分片还会运行所有未分配给其他分片的测试套件。

构建阶段#

使用 tuistPrepareTestShards 任务准备测试分片:

bash
./gradlew tuistPrepareTestShards \
-PtuistShardMax=5

此任务:

  1. 查找包含待分片测试任务的项目,且不编译它们
  2. 根据过去 30 天为这些项目记录的测试套件和耗时,在 Tuist 服务器上创建分片计划
  3. 为你的 CI 系统输出分片矩阵

当尚未记录任何测试套件时(例如首次运行),计划将包含一个运行所有测试的单一分片。

构建选项#

通过 Gradle 项目属性配置分片:

属性描述
-PtuistShardMax=<N>最大分片数(默认值:2)
-PtuistShardMin=<N>最小分片数
-PtuistShardMaxDuration=<MS>每个分片的目标最大持续时间(毫秒)
-PtuistShardTestTask=<NAME>要分片的测试任务,例如 testDebugUnitTest。默认为构建中的每个测试任务

具有多个构建变体的 Android 项目,每个变体都有一个单元测试任务。将 -PtuistShardTestTask 设置为你的分片所运行的变体,以便仅规划包含该变体的项目:

bash
./gradlew tuistPrepareTestShards \
-PtuistShardMax=5 \
-PtuistShardTestTask=testDebugUnitTest

分片引用会自动从 CI 环境变量(GITHUB_RUN_ID、CI_PIPELINE_ID 等)派生,或者可以通过 TUIST_SHARD_REFERENCE 环境变量显式设置。

测试阶段#

每个分片运行器使用标准的 test 任务执行其分配的测试。当设置了 TUIST_SHARD_INDEX 时,插件会自动从服务器获取分片分配,并过滤测试执行以仅包含分配的测试套件。没有为该分片分配任何套件的项目中的测试任务将被跳过。

最后一个分片会运行每个测试任务,并排除分配给其他分片的套件,因此尚未有记录运行的套件(例如新添加的套件)仍会运行。

bash
TUIST_SHARD_INDEX=0 ./gradlew test

如果你在构建阶段设置了 -PtuistShardTestTask,请在每个分片中运行相同的任务,例如 ./gradlew testDebugUnitTest。

持续集成#

Tuist 会自动检测以下 CI 提供商:

对于其他提供商,请参阅 .tuist-shard-matrix.json 文件以设置并行任务。

每个构建和分片作业在运行 Gradle 之前都需要进行身份验证。你可以安装 Tuist CLI 并使用你的 CI 提供程序的 OIDC 身份运行 tuist auth login,或者将账户令牌作为 TUIST_TOKEN 公开。请参阅Gradle 插件身份验证。以下示例省略了该步骤。

GitHub Actions#

使用矩阵策略并行运行分片:

yaml
name: Tests
on: [pull_request]
jobs:
build:
name: Build test shards
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.build.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- uses: gradle/actions/setup-gradle@v4
- id: build
run: ./gradlew tuistPrepareTestShards -PtuistShardMax=5
test:
name: "Shard #${{ matrix.shard }}"
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: ${{ fromJson(needs.build.outputs.matrix).shard }}
env:
TUIST_SHARD_INDEX: ${{ matrix.shard }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- uses: gradle/actions/setup-gradle@v4
- run: ./gradlew test

GitLab CI#

Tuist 会生成一个 .tuist-shard-child-pipeline.yml,你可以将其作为子流水线触发。定义一个 .tuist-shard 模板作业,生成的分片作业将扩展该模板:

yaml
# .gitlab-ci.yml
stages:
- build
- test
build-shards:
stage: build
script:
- ./gradlew tuistPrepareTestShards -PtuistShardMax=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

子流水线需要 .tuist-shard 作业模板:

yaml
# .gitlab/shard-template.yml
.tuist-shard:
script:
- ./gradlew test

CircleCI#

Tuist 会生成一个 .tuist-shard-continuation.json,其中包含用于continuation orb的参数:

yaml
# .circleci/config.yml
version: 2.1
setup: true
orbs:
continuation: circleci/continuation@1
jobs:
build-shards:
docker:
- image: cimg/openjdk:17.0
steps:
- checkout
- run:
name: Build and plan shards
command: |
./gradlew tuistPrepareTestShards -PtuistShardMax=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
jobs:
test-shard:
docker:
- image: cimg/openjdk:17.0
parameters:
shard-index:
type: integer
steps:
- checkout
- run:
name: Run shard
command: |
export TUIST_SHARD_INDEX=<< parameters.shard-index >>
./gradlew test
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: |
./gradlew tuistPrepareTestShards -PtuistShardMax=5
buildkite-agent pipeline upload .tuist-shard-pipeline.yml

每个生成的步骤都在其环境中设置了 TUIST_SHARD_INDEX。使用共享脚本将测试命令添加到每个分片步骤:

bash
# .buildkite/shard-step.sh
#!/bin/bash
./gradlew test

Codemagic#

Codemagic 不支持动态矩阵作业,因此为每个分片定义单独的工作流。Tuist 会将 TUIST_SHARD_MATRIX 和 TUIST_SHARD_COUNT 写入 CM_ENV 文件,以便在每个工作流中使用:

yaml
# codemagic.yaml
workflows:
build-shards:
name: Build test shards
instance_type: linux_x2
environment:
java: 17
scripts:
- name: Build and plan shards
script: |
./gradlew tuistPrepareTestShards -PtuistShardMax=5
test-shard-0: &shard-workflow
name: "Shard #0"
instance_type: linux_x2
environment:
java: 17
vars:
TUIST_SHARD_INDEX: 0
scripts:
- name: Run shard
script: |
./gradlew test
test-shard-1:
<<: *shard-workflow
name: "Shard #1"
environment:
java: 17
vars:
TUIST_SHARD_INDEX: 1
test-shard-2:
<<: *shard-workflow
name: "Shard #2"
environment:
java: 17
vars:
TUIST_SHARD_INDEX: 2
test-shard-3:
<<: *shard-workflow
name: "Shard #3"
environment:
java: 17
vars:
TUIST_SHARD_INDEX: 3
test-shard-4:
<<: *shard-workflow
name: "Shard #4"
environment:
java: 17
vars:
TUIST_SHARD_INDEX: 4

Bitrise#

在 Bitrise 上,Tuist 会将 .tuist-shard-matrix.json 写入 BITRISE_DEPLOY_DIR,使其可作为构建产物供下游流水线阶段使用。使用 Bitrise Pipelines 并配置预定义的并行工作流:

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: |
./gradlew tuistPrepareTestShards -PtuistShardMax=5
test-shard-0: &shard-workflow
envs:
- TUIST_SHARD_INDEX: 0
steps:
- script:
title: Run shard
inputs:
- content: |
./gradlew test
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 不支持在运行时动态创建并行作业。请在流水线阶段中定义固定数量的分片工作流——同一阶段内的工作流会自动并行运行。