如何在Github Actions中并行运行测试(附代码)

374 阅读5分钟

我所在的公司最近改用Github Actions做CI,因为他们提供每月2000分钟的服务,这对我们的工作量来说已经很足够了。我们一直在以前的CI上并行运行测试(2个实例)以节省时间,我们想在Github Actions上也这样做。

Github Actions提供了策略矩阵,可以让你并行运行测试,每个矩阵有不同的配置。

例如,下面是一个矩阵策略:

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    strategy:
      matrix:
        ruby:
          - '2.5.x'
          - '2.6.x'
          - '2.7.x'
        activerecord:
          - '5.0'
          - '6.0'
          - '6.1'

它将并行运行9个测试,3个版本的Ruby x 3个版本的ActiveRecord = 9个测试

9 tests

Github Actions的一个缺点是,他们没有内置的分割测试功能,可以在不同的CI实例中分割测试文件,而许多其他CI都有这样的功能。例如,CircleCI提供了像这样的分割测试命令:

# Run rspec in parallel, and split the tests by timings
- run:
    command: |
      TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
      bundle exec rspec $TESTFILES

通过分割测试,我们可以在不同的CI实例上运行不同的测试文件以减少每个实例上的测试执行时间。

幸运的是,我们可以在Ruby中编写自己的脚本来分割测试文件。

在我们继续编写分割测试脚本之前,让我们配置 Github Actions 工作流以并行运行我们的测试。

让我们配置我们的Github Actions工作流程来并行运行测试:

# .github/workflows/test.yml
name: test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        # Set N number of parallel jobs you want to run tests on.
        # Use higher number if you have slow tests to split them on more parallel jobs.
        # Remember to update ci_node_index below to 0..N-1
        ci_node_total: [2]
        # set N-1 indexes for parallel jobs
        # When you run 2 parallel jobs then first job will have index 0, the second job will have index 1 etc
        ci_node_index: [0, 1]
    env:
      BUNDLE_JOBS: 2
      BUNDLE_RETRY: 3
      BUNDLE_PATH: vendor/bundle
      PGHOST: 127.0.0.1
      PGUSER: postgres
      PGPASSWORD: postgres
      RAILS_ENV: test
    services:
      postgres:
        image: postgres:9.6-alpine
        ports: ["5432:5432"]
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        env: 
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: bookmarker_test

    steps:
    # ...

ci_node_total指的是你想在CI过程中启动的并行实例的总数。我们在这里使用2个实例,所以数值为[2]。如果你想使用更多的实例,比如4个实例,那么你可以把这个值改为 [4] 。

ci_node_index指的是你在CI过程中旋转的并行实例的索引,这应该与你之前定义的ci_node_total相匹配。

例如,如果你总共有2个节点,你的ci_node_index应该是[0, 1]。如果你有4个总节点,你的ci_node_index应该是[0, 1, 2, 3]。这对我们以后写脚本分割测试时很有用。

fail-fast: false表示即使其中一个实例的测试失败,我们也要继续在其他实例上运行测试。如果我们没有设置,fail-fast的默认值是true,如果一个实例上有一个失败的测试,它将停止所有的实例。设置fail-fast: false将允许所有的实例完成他们的测试,并且我们可以在以后看到哪个测试失败了。

接下来,我们在工作流中添加测试步骤:

steps:
  # ... Ruby / Database setup...
  # ...
  - name: Make bin/ci executable
    run: chmod +x ./bin/ci
  - name: Run Rspec test
    id: rspec-test
    env:
      # Specifies how many jobs you would like to run in parallel,
      # used for partitioning
      CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
      # Use the index from matrix as an environment variable
      CI_NODE_INDEX: ${{ matrix.ci_node_index }}
    continue-on-error: true
    run : |
      ./bin/ci

我们将ci_node_totalci_node_index这两个变量传入测试步骤的环境变量**(env:**)。

continue-on-error: true将指示工作流继续进行下一步,即使测试步骤出现错误(即任何测试失败)。

最后,我们要运行自定义的测试脚本,将测试文件分割开来并运行它们**。**

我们将在下一节中研究如何编写bin/ci

准备分割测试文件并运行它们的ruby脚本

在本节中,我们将编写bin/ci文件。

首先,创建一个名为 "ci"的文件(没有文件扩展名),并将其放在你的Rails应用程序的 "bin"文件夹中,像这样。ci_file_location

下面是ci文件的代码:

#!/usr/bin/env ruby

tests = Dir["spec/**/*_spec.rb"].
  sort.
  # Add randomization seed based on SHA of each commit
  shuffle(random: Random.new(ENV['GITHUB_SHA'].to_i(16))).
  select.
  with_index do |el, i|
    i % ENV["CI_NODE_TOTAL"].to_i == ENV["CI_NODE_INDEX"].to_i
  end

exec "bundle exec rspec #{tests.join(" ")}"

Dir["spec/**/*_spec.rb"] 将返回一个测试文件的路径数组,这些文件位于spec文件夹内,文件名以**_spec.rb**结尾。(例如:["spec/system/bookmarks/create_spec.rb", "spec/system/bookmarks/update_spec.rb"] )。

如果你使用minitest而不是Rspec,你可以将其改为Dir["test/**/*_test.rb"]

sort将按照字母顺序对测试文件的路径阵列进行排序。

然后shuffle将随机化数组的顺序,使用随机参数中提供的种子,因此随机化的顺序将根据我们传入的随机参数而不同。

**ENV['GITHUB_SHA']**是触发Github Actions工作流的提交的SHA哈希值,可作为Github Actions的环境变量。

由于Random.new只接受种子参数的整数,我们必须将SHA哈希值(十六进制字符串,例如:fe5300b3733d69f0a187a5f3237eb05bb134e341)转换成整数,使用十六进制为基础(.to_i(16))。

然后我们使用select.with_index来选择将要运行的测试文件。对于每个CI实例,我们将从数组中选择测试文件,其中索引的余数除以总节点数等于CI实例索引。

例如,假设CI_NODE_TOTAL = 2,CI_NODE_INDEX = 1,并且我们有四个测试文件。

explanation

测试文件将如上所示被分割。

这种分割的缺点是,测试不会根据测试的持续时间进行分割,这可能会使一些实例的运行时间比其他的长。CircleCI确实存储了每个测试持续时间的历史数据,可以根据时间来分割测试文件,希望Github Actions在未来能实现这个功能。