Skip to content
On this page

📆 2022-07-22

Java アプリケーションのテスト時間を短縮した

#GitHub Actions #Java

はじめに

Java アプリケーションのCIでのテスト時間短縮が長いので改善した話。

対象のプロジェクトでは、GtiHub の1つのリポジトリで10以上のJavaアプリケーションを管理している。

コミットをプッシュしたら、全アプリケーションの全単体テストがテストされるのだが、これがだいたい20分〜30分ほど掛かっていて、結構辛い。

サービスが成長してアプリケーションが増えていくと、さらに時間がかかりそうだったので、手を打つことにした。

やったこと

  • テストの並列化
  • 変更したアプリケーションだけテスト

対象のプロジェクト

複数の Java アプリケーションを Gradle のマルチプロジェクト構成でまとめている。

イメージはこんな感じ

hoge-project
├── applicationA
│   └── src
│       ├── main
│       └── test
├── applicationB
│   └── src
│       ├── main
│       └── test
├── ...
├── ...
├── ...
├── build.gradle.kts
└── settings.gradle.kts

GtiHub に feature ブランチをプッシュすると、GitHub Actions のワークフローで全アプリケーションの全テストを実行している。

具体的には gradle build を実行していて、その中で単体テストや ktlint が実行されていて、エラーがあればワークフローが失敗する。

GitHub Actions のワークフローがこちら

name: test

on:
  push:
    branches-ignore:
      - main

jobs:
  test:
    name: test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          ...
      - name: Test
        run: ./gradlew build

ワークフローが失敗するとその feature ブランチは main ブランチにマージできないように制限されている。制限は、GitHub の Branch protection rule でやっている。

改善

テストの並列化

GitHub Actions の matrix strategy を使って各アプリケーションのテストを並列で実行することにした。

これで最もテストに時間にかかるアプリケーションに律動してCIが終わるようになったので、全体のビルド時間がかなり短縮された。

長いもので5分〜10分ほどかかっていたので、だいたいその時間でCIが終わるようになった。

GitHub Action のワークフローはこうなる

...

jobs:
  setup-matrix:
    name: setup-matrix
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup matrix
        id: setup-matrix
        run: |
          apps=$(cat "apps.csv")
          echo ::set-output name=applications::[$apps]
    outputs:
      apps: ${{ steps.setup-matrix.outputs.apps }}
  test:
    name: test
    needs: setup-matrix
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 20
      matrix:
        app: ${{ needs.setup-matrix.outputs.apps }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup java runtime
        uses: actions/setup-java@v3
        with:
          ...
      - name: Build
        run: ./gradlew :${{ matrix.app }}:build

  all_test:
    name: all_test
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - name: Finish
        run: echo "done"

apps.csvは、アプリケーションの名前の一覧が入ったcsvファイル

applicationA, applicationB, ...

GitHub の Branch protection rule > Require branches to be up to date before merging には all_test を選択しておく。これで全部のテストがとおらないと main ブランチにマージできないようになる。

変更したアプリケーションだけテスト

変更したアプリケーションだけテストすることにした。

もちろん共通ライブラリに手を入れたときは、全アプリケーションのテストが必要なので、そのあたりのチェックが必要になる。

全アプリケーションに影響するような変更は稀なので、この改善で、小さな変更を頻繁にリリースするときのリードタイムが改善された。

小さなアプリケーションなら3分未満でテストが終わるので、いままで20〜30分待っていたことを考えると、かなりうれしい。

差分チェックするシェルスクリプトを作った。

IGNORES=".github,docs,README.md,.gitignore,settings-modules.github"
ALL_APPS_FILE="apps.csv"

if [ ! -f $APPS_FILE ]; then
  echo "ERR! $APPS_FILE not found" >&2
  exit 1
fi
ALL_APPS=$(cat $ALL_APPS_FILE)

## 差分があるトップレベルのファイルとディレクトリを取得
git fetch --depth 1 origin main
DIFF="$(git diff origin/main HEAD --name-only | awk -F'/' '{print $1}' | uniq)"

## 差分から無視リストを除外
for ignore in ${IGNORES//,/ } ; do
  DIFF="$(echo "$DIFF" | sed "s/^${ignore}\$//g")"
done

## 変更されたモジュールをビルド対象にする
APPS=''
for app in ${ALL_APPS} ; do
  d="$(echo "$DIFF" | sed "s/^${app}\$//g")"
  if [ "$d" != "$DIFF" ]; then
    APPS=${APPS}${module}`,`
  fi
  DIFF="$d"
done

## モジュール以外のファイルが変更されていたらすべてビルドする
if [ "$(echo $DIFF | tr -d '\n')" != "" ]; then
  APPS=$ALL_APPS
fi

echo "$APPS"

🔥 このシェルスクリプトは、実際のものから多少変更しているので動かないかもしれません。 またエラーアンドリングも甘いし可読性も低いので書き直したい気持ちもあります。 あくまで参考までに載せます。

上記のシェルスクリプトを diff.sh という名前で保存しておく。

GitHub Action のワークフローはこうなる

...

jobs:
  setup-matrix:
    name: setup-matrix
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup matrix
        id: setup-matrix
        run: |
          apps=$(bash ./diff.sh)
          echo "::set-output name=modules::[$apps]"
    outputs:
      apps: ${{ steps.setup-matrix.outputs.apps }}
  test:
    name: test
    needs: setup-matrix
    runs-on: ubuntu-latest
    if: ${{ needs.setup-matrix.outputs.apps != '[]' }}
    strategy:
      max-parallel: 20
      matrix:
        app: ${{ needs.setup-matrix.outputs.apps }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup java runtime
        uses: actions/setup-java@v3
        with:
          ...
      - name: Build
        run: ./gradlew :${{ matrix.app }}:build
  all_test:
    name: all_test
    needs: [test]
    runs-on: ubuntu-latest
    steps:
      - name: Finish
        run: echo "done"

まとめ

かなり改善できたのでは 🎉

  • テストの並列化で、最もテストに時間にかかるアプリケーションに律動してCIが終わるようになったので、全体のビルド時間がかなり短縮された。

  • 変更したアプリケーションだけテストすることで、小さな変更を頻繁にリリースするときのリードタイムが改善された。

ソースコードを触った後、テストやビルド、リリース作業に時間を取られてしまうのは本質的ではないので、まだまだ改善して快適なCI/CD環境にしていきたい。

Released under the MIT License.