生きてると、テストがどんどん遅くなりがちです。実際にDBに接続するタイプのテストとか……。そしてそういう種類のテストに限って並列化しやすいように実装されていないこともありがちですよね。
GitHub Actionsを使っている場合*1、そのようなテストが複数ファイル・パッケージにわたる場合は独立したジョブでテストを走らせることでCIの高速化を見込むことができます。
ということで、特定のパッケージのテストだけ独立したジョブで走らせる・残りのパッケージのテストは単一のジョブで走らせる という仕組みを作りました。実物を見たほうが早いと思うので、以下のリポジトリを見てください。
鍵となるのは以下3つのファイルです。応用すればGoに限らずさまざまな言語のテストフレームワーク向けに使えるはず。
- separated-test-pkgs.txt
- separate-test-pkg.pl
- .github/workflows/ci.yml
separated-test-pkgs.txt
このテキストファイルに、独立したランナーで走らせたいパッケージ名を1行1エントリで書きます。パッケージ名の形式は go list ./...
コマンドで出力されるものに揃えてください。
separate-test-pkg.pl
いきなりPerlのコードが出てきましたね。PerlはGitHubが提供するGitHub Actionsのrunnerに入っている*2ので、おもむろに小さなスクリプトを書くのに便利です。順に読んでいきましょう。
use strict; use warnings; use JSON::PP qw(encode_json);
JSON::PPはPerl v5.13.9からコアモジュール*3になっているので、追加でモジュールをインストールする必要なく使えます。よかったですね。
# separated-test-pkgs.txt から、個別にテストを実行したいパッケージを取得する my @separated_test_pkgs = do { open my $fh, '<', 'separated-test-pkgs.txt' or die $!; chomp (my @xs = <$fh>); close $fh; @xs; }; # package名 => 個別にテストを実行したいなら true my %is_separated_test_pkg = map { $_ => 1 } @separated_test_pkgs;
先述した separated-test-pkgs.txt ファイルから、個別にテストしたいパッケージの一覧を取得しています。
%is_separated_test_pkg
変数はあとで使います。
# このモジュール下のパッケージ一覧を取得する my @all_pkgs = split "\n", `go list ./...`; # 同時にテストを実行してよいパッケージのみを抽出する my @independent_pkgs = grep { !$is_separated_test_pkg{$_} } @all_pkgs;
バッククォートで囲んだ文字列はシェルコマンドとして実行され、実行結果の文字列が入ります。
go list ./...
コマンドの実行結果をsplitすることで、全パッケージ一覧を取得しています。
先ほど組み立てた %is_separated_test_pkg
変数を使って、個別にテストしなくてよいパッケージの一覧を抽出しています。
# JSON形式で出力する (フォーマットは __END__ 以下を参照) my $outputs = [ ( map { +{ name => $_, packages => [$_], } } @separated_test_pkgs ), +{ name => 'Others', packages => [@independent_pkgs], }, ]; print encode_json $outputs; __END__ (省略)
GitHub Actions workflowのジョブのoutputとして使いやすいように、JSON形式で出力しています。{"name":"テストの名前","packages":[パッケージ一覧]}
というオブジェクトの配列を作っています。
.github/workflows/ci.yml
name: CI on: push: branches: - main pull_request: jobs: prepare: name: Prepare runs-on: ubuntu-latest outputs: separated-pkgs: ${{ steps.separate.outputs.out }} steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: go-version-file: go.mod - id: separate run: echo "out=$(perl separate-test-pkg.pl)" >> $GITHUB_OUTPUT test: name: Test (${{ matrix.target.name }}) needs: - prepare runs-on: ubuntu-latest strategy: fail-fast: false matrix: target: ${{ fromJSON(needs.prepare.outputs.separated-pkgs) }} steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: go-version-file: go.mod - run: go test -v ${{ join(matrix.target.packages, ' ') }}
prepareジョブで、どのテストを独立に実行するか・同時に実行するか を先述した separate-test-pkg.pl の出力によって決めています。
prepareジョブの出力を受けて、testジョブでは独立にテストを実行するパッケージのmatrixと、それ以外のパッケージのmatrixに分けてテストを走らせています。go test
コマンドには複数のパッケージ名を渡せるので、これでテストを個別に実行できるわけですね。
fromJSON
や join
はGitHub Actions workflowの組み込み関数です*4。
動的に組み立てたmatrixのフィールドは matrix.target
オブジェクトから参照できます。
fail-fast
には false
を指定しておいて、1つのテストジョブがコケても他のジョブが即時終了しないようにしておくほうがよいでしょう。
実際に以上のworkflowをもとにCIを走らせると、以下の画像のようにジョブが分かれていることが確認できます。
先行研究
gotesplit*5というツールを使うと、Goのテストを分割数を指定して独立したジョブとして走らせることができるようです。
今回は、DBを使うテストだけ独立して実行する・DBを使わないテストは高速に終わるからまとめて実行する というふうに、分割の基準を自由に指定できるようにしたかったので、自前で仕組みを用意してみました。このような単位で分けておくと、DBを使うテストでだけDBの準備をするようにできるし、複数のパッケージのテストからDBにアクセスしても干渉しないようにできます*6。