我所在的公司最近改用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个测试

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_total和ci_node_index这两个变量传入测试步骤的环境变量**(env:**)。
continue-on-error: true将指示工作流继续进行下一步,即使测试步骤出现错误(即任何测试失败)。
最后,我们要运行自定义的测试脚本,将测试文件分割开来并运行它们**。**
我们将在下一节中研究如何编写bin/ci。
准备分割测试文件并运行它们的ruby脚本
在本节中,我们将编写bin/ci文件。
首先,创建一个名为 "ci"的文件(没有文件扩展名),并将其放在你的Rails应用程序的 "bin"文件夹中,像这样。
下面是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,并且我们有四个测试文件。

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