📙

CircleCI上でgo言語のユニットテストを差分実行する方法

2023/10/03に公開

背景

リモートブランチにコミット・プッシュする度に、CircleCI を用いて go 言語で書かれたユニットテストを実行していたのですが、
全てのユニットテストを実行するのに約 20 分要していました。

ゴール

変更箇所にのみテストを実行することで、テスト実行時間を短縮する。

やってみた結果

変更ファイル数にも依存しますが、1度のPRで生じるファイル変更数くらいであれば、
テスト実行に要する時間を約 20 分 → 約 4 分に短縮することができました。

差分実行にする前のテスト実行方法

- run:
  name: Run tests & make report
  command: |
    echo "" > coverage.txt
    for package_path in $(go list ./... ); do
        go test -v -race -coverprofile=profile.out -covermode=atomic "$package_path"
        if [ -f profile.out ]; then
            cat profile.out >> coverage.txt
            rm profile.out
        fi
    done

上記のコマンドによって、カレントディレクトリ以下の全てのパッケージをテスト実行し、カバレッジを取得することができます。
以下に、上記コマンドの重要な部分を抜粋します。

  • go list ./...:カレントディレクトリ以下の全てのパッケージパスを取得する
  • go test "$package_path":取得したパッケージパスをテスト実行する。ファイル名やパッケージ名を指定することもできる。

go testに以下のオプションを指定しています。

  • -v:テスト実行時に詳細なログを出力する
  • -race:データ競合を検知する
  • -coverprofile=profile.out:カバレッジを取得する
  • -covermode=atomic:カバレッジを取得する際に、複数のテスト実行結果を統合する

差分実行にする方法

ブランチ名は環境変数$CIRCLE_BRANCH で取得

masterブランチなどのルートブランチでは保守性と品質確保の観点から、全てのユニットテストを実行したいです。
一方で、それ以外のフィーチャーブランチやトピックブランチでは、開発の高速化のために、変更箇所にのみテストを実行したいです。
CircleCI では、環境変数$CIRCLE_BRANCHで現在のブランチ名を取得することができます。
従って、$CIRCLE_BRANCH を用いて、master ブランチかどうかを判定します。

if [ "$CIRCLE_BRANCH" = "master" ]; then
    echo "Branch is master"
else
    echo "Branch is not master"
fi

git diffでmasterブランチとの差分を取得

git diff ブランチ名

で、現在のブランチと指定したブランチの差分ファイルを取得することもできます。

また、

git diff HEAD^

とすることで、直前のコミットとの差分ファイルを取得することができます。

--diff-fileter オプションで取得するファイルを絞り込む

gitの公式ドキュメントによると、

--diff-filter=[(A|C|D|M|R|T|U|X|B)…​[*]]
Select only files that are Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R), have their type (i.e. regular file, symlink, submodule, …​) changed (T), are Unmerged (U), are Unknown (X), or have had their pairing Broken (B). Any combination of the filter characters (including none) can be used. When * (All-or-none) is added to the combination, all paths are selected if there is any file that matches other criteria in the comparison; if there is no file that matches other criteria, nothing is selected.

Also, these upper-case letters can be downcased to exclude. E.g. --diff-filter=ad excludes added and deleted paths.
Note that not all diffs can feature all types. For instance, copied and renamed entries cannot appear if detection for those types is disabled.

要は、--diff-filter=で指定したフラグに該当する変更ファイルのみを取得することができます。

--diff-filter=ACMR

  • A: ファイルが新たに追加された
  • C: ファイルがコピーされた
  • M: ファイルが変更された
  • R: ファイルが名前を変更した

削除されたファイルを go test の対象とすると、テスト実行時にエラーが発生するため、D は含めませんでした。

ファイルパスをパッケージパスに変換する

git diff で取得したファイルパスを、go test で指定するパッケージパスに変換する必要があります。
例えば、src/controller/hoge/fuga.go というファイルが変更された場合、そのファイルパスをcontroller/hoge というパッケージパスに変換します。

package_path=$(echo "$file_name" | sed 's#^src/##' | sed 's#/[^/]*$##')

差分実行コマンドまとめ

以上のことを踏まえた、最終的な差分実行コマンドは以下のようになります。

run:
    name: Run tests & make report
    command: |
        echo "" > coverage.txt

        if [ "$CIRCLE_BRANCH" = "master" ]; then
            echo "Branch is master"
            for package_path in $(go list ./... ); do
                go test -v -race -coverprofile=profile.out -covermode=atomic "$package_path"
                if [ -f profile.out ]; then
                    cat profile.out >> coverage.txt
                    rm profile.out
                fi
            done
          else
            echo "Branch is not master"
            visited_packages=()
            for file_name in $(git diff origin/master...HEAD --name-only --diff-filter=ACMR | grep "\.go$"); do
              package_path=$(echo "$file_name" | sed 's#^src/##' | sed 's#/[^/]*$##')
              if [[ " ${visited_packages[@]} " =~ " ${package_path} " ]]; then
                  continue
              fi
              visited_packages+=("$package_path")
              go test -v -race -coverprofile=profile.out -covermode=atomic "$package_path"
              if [ -f profile.out ]; then
                  cat profile.out >> coverage.txt
                  rm profile.out
              fi
            done
          fi
          echo "" > coverage.txt

参考文献

CircleCIの公式ドキュメント
https://circleci.com/docs/ja/

CircleCIではなく、GitHub Actions を用いて差分実行をしたい場合は、こちらの記事が参考になります。
https://zenn.dev/mitsugu/articles/6f4b5f1be6b20a

GitHubで編集を提案

Discussion