目次:
- はじめに
- 前提知識
- 当時の課題
- 実施した改善策
- 結果
- その他
- まとめ
はじめに
こんにちは。今年に入って2ヶ月に1回以上K-POPなどのライブに行っている、楽楽債権管理開発チームの冨澤です。
楽楽債権管理は新サービスとして2025年7月1日から販売を開始した、ラクスの中では比較的新しいサービスであり、高速に開発することが求められます。
本記事ではそんな高速開発を支える取り組みとして、CIのテスト実行時間を短縮した話をご紹介したいと思います。
前提知識
本記事での取り組みでは、以下の内容を前提としています。
対応時期
2025年3月
技術スタック
- 言語
- Java 21
- FW
- Spring Boot 3
- テストツール
- Spock
- ビルドツール
- Gradle(Groovy)
- CI
- GitHub Actions
- アーキテクチャ
- オニオンアーキテクチャ
1つのGradleプロジェクトで管理しており、/app
配下に処理に必要なディレクトリが存在し、単体テストコードも実装と同じパッケージ構成で用意しています。
また層を跨ぐ依存関係にはテストダブルを利用することで、各層の単体テストを独立して実行できるようにしています。
以下はパッケージ構成の例です(物理パスはGradle標準のsrc/main/java、テストはsrc/test/groovyなど各プロジェクト標準に準拠)
src/main/.../app ├── controller ├── domain ├── infrastructure ├── persistence └── usecase src/test/.../app ├── controller ├── domain ├── infrastructure ├── persistence └── usecase
改善前のCIワークフロー設定
- lintジョブ
- Spotlessを使ったチェック
- unit-testジョブ
- DBコンテナを起動
- DBマイグレーション
- 単体テスト実行
- カバレッジの集計
当時の課題
プロダクトフェーズから考えると、CIの時間はもちろん早い方がいいです。 ですが、当時は以下のような状況でした。
- 計測時期
- 2025年3月
- テストの平均実行時間(成功のみ)
- 20分50秒
成功したワークフローのみを対象としたのは、失敗したワークフロー(ビルド or テスト失敗や、cancel-in-progress
による早期打ち切りなど)を含めると平均実行時間が実態より短く算出され、改善効果を正確に測定できないためです。
実行時間が長い主因として、まず直列実行を疑いました。 キャッシュの未使用やマシンスペックの問題も考えられましたが、小さく早く試して効果を検証するという観点から、設定変更で実現できる並列実行が最も着手しやすい改善策だと判断しました。
そこで今回は「テストジョブの並列実行」を第一の改善策として採用し、その効果を検証することにしました。
実施した改善策
- lintジョブ
- Spotlessを使ったチェック
- 4並列のテスト実行ジョブ
- A
- B
- C
- D
- カバレッジジョブ
- 各ジョブで実施した内容を集計
GradleとGitHub Actionsの組み合わせ
テストジョブの並列実行を実現するために、GradleとGitHub Actionsの2つを組み合わせました。
①:Gradleでカスタムタスクの作成
Gradleでの並列化としてよくmaxParallelForks
が紹介されますが、これは単一のタスク内でJVMプロセスの並列度を上げるための設定です。
一方、今回の取り組みではCI全体の実行時間を効率よく短縮することを目的とし、GitHub Actionsのジョブそのものを分割して並列実行できるようにする必要がありました。
そこで、workflowのmatrix戦略にそのまま割り当てられる実行単位としてカスタムタスクをGradle 側に定義しました。
この方法により、特定のテストグループをジョブ単位で独立して制御できるようになりました。加えて、カバレッジの集約も簡素化され、将来的な差分テストなどへの拡張性も確保できました。
以下にその定義例を示します。
// 共通のテスト設定 tasks.withType(Test).configureEach { useJUnitPlatform() } tasks.register('A', Test) { include '**/persistence/**/*.*', '**/infrastructure/**/*.*', '**/domain/**/*.*' } tasks.register('B', Test) { include '**/usecase/a**/**/*.*', '**/usecase/b**/**/*.*', '**/usecase/c**/**/*.*', '**/usecase/d**/**/*.*', '**/usecase/e**/**/*.*', '**/usecase/f**/**/*.*', '**/usecase/g**/**/*.*', '**/usecase/h**/**/*.*', '**/usecase/i**/**/*.*', '**/usecase/j**/**/*.*', '**/usecase/k**/**/*.*', '**/usecase/l**/**/*.*' } tasks.register('C', Test) { include '**/usecase/m**/**/*.*', '**/usecase/n**/**/*.*', '**/usecase/o**/**/*.*', '**/usecase/p**/**/*.*', '**/usecase/q**/**/*.*', '**/usecase/r**/**/*.*', '**/usecase/s**/**/*.*', '**/usecase/t**/**/*.*', '**/usecase/u**/**/*.*', '**/usecase/v**/**/*.*', '**/usecase/w**/**/*.*', '**/usecase/x**/**/*.*', '**/usecase/y**/**/*.*', '**/usecase/z**/**/*.*' } tasks.register('D', Test) { exclude '**/persistence/**', '**/infrastructure/**', '**/domain/**/*.*', '**/usecase/**' }
./gradlew test --tests hoge
から着想を得ました。
また公式ドキュメントにも「テストのグループ化」というセッションで、似たような方法を紹介しており、こちらも参考にしました。
またこの4つの分け方は、実行時間が均等になるように調整しました。
なぜこの書き方なのか
GradleのTestタスクは、正規表現ではなくAnt形式の include/exclude パターンをサポートしています。 クロージャーやSpecを利用した柔軟なフィルタリングもできますが、当時はとにかく早く並列化を実現したかったので、Ant形式を採用しました。
②:GitHub Actionsでのジョブの並列実行
こちらはmatrix
を使って、並列実行を実現しました。
unit-test: strategy: matrix: test-task: [ A, B, C, D ]
シンプルに、先ほど作成したカスタムタスクを指定し、並列で実行するように設定しました。
③:JaCoCoレポートの作成
改善前は、1つのジョブで全てのテストを実行していたのでJaCoCoレポートの作成も容易でした。
しかし、テスト実行ジョブを4つに分割して並列化したことで、実行結果をうまくマージしないとJaCoCoレポートが作成できない問題に直面しました。
これに関しては、各テストジョブ実行後にclasses
ディレクトリとjacoco/*.exec
をアーティファクトにアップロードし、カバレッジジョブで集計するようにしました。
レポートを作成しないと、リファクタリングの結果(カバレッジは変わっていないが、実行時間が短縮されている)を正確に確認できないので、まずはこちらを先に設定することをおすすめします。
以下にその定義例を示します。
# 各テストジョブ - name: Upload Compiled Classes and Jacoco Execution Data uses: actions/upload-artifact@n.n.n with: name: compiled-classes-and-jacoco-${{ matrix.test-task }} path: | build/classes **/build/jacoco/*.exec retention-days: 1 # カバレッジジョブ - name: Download Combined Artifact (Compiled Classes & Jacoco Data) uses: actions/download-artifact@n.n.n with: pattern: 'compiled-classes-and-jacoco-*' path: build/download-artifacts - name: Restore Compiled Classes and Jacoco Exec Files run: | mkdir -p build/classes mkdir -p build/jacoco # 各アーティファクトディレクトリからクラスファイルと.execファイルを抽出 for d in build/download-artifacts/*; do echo "Processing artifact directory: $d" if [ -d "$d/build/classes" ]; then cp -R "$d/build/classes/." build/classes/ fi find "$d" -name "*.exec" -exec cp {} build/jacoco/ \; done - name: Generate Jacoco Report run: ./gradlew jacocoTestReport --info
結果
日付 | 平均実行時間(成功のみ) | テスト実行方法 |
---|---|---|
2025年3月 | 20分50秒 | 直列 |
2025年3月 | 10分40秒(最短10分5秒) | 並列 |
2025年4月 | 12分5秒 | 並列 |
2025年5月 | 14分26秒 | 並列 |
2025年6月 | 12分5秒 | 並列 |
2025年7月 | 13分41秒 | 並列 |
2025年8月 | 14分47秒 | 並列 |
テスト数は増え続けています(3月から8月で、約1,000件増加)が平均実行時間にばらつきがあるのは、GitHub Actionsのランナーリソースが他のワークフローと競合する時間帯に、実行時間が伸びる傾向があるからです。 そのため、開発が活発な期間やバグを集中改修する期間は同時実行数が増え、ほぼ同じテスト数でもリソース待ちが発生し実行時間が伸びてしまっています。
その他
CI実行時間の短縮に直接関係のある話ではないですが、この対応に関連して行ったことを少し整理してみます。
費用対効果の確認と事前調整
今回の対応によってCIのテスト実行時間が短縮されること自体は望ましいですが、並列化によりジョブ数が増えるため、その分消費するGitHub Actions の利用時間も増加します。
私たちの組織では、Enterprise単位で利用可能なGitHub Actionsの分数に上限が設けられているため、この増加が他チームの上限枠を圧迫する可能性を考慮する必要がありました。
今回の対応はCPU/OS 種別の変更を伴わず、あくまでジョブ数の増加による実行時間消費への影響が論点となります。
そのため、改善策を導入する前に社内の横断的な技術サポート組織である開発管理課に相談し、利用状況や上限枠を確認しました。 あわせて、公開情報を参考にコストと削減時間を試算し、想定される費用対効果を数値として提示しました。
その結果、管理職から速やかに承認を得ることができ、問題になる前に先に相談したことで、気持ち的にも余裕を持って対応することができました。
gh
コマンド+Claude Codeを使ったワークフロー統計分析
今回の対応を実施し、毎月の推移を集計・確認していました。 内容としては成功したワークフローの実行時間を集計し、平均実行時間を出していました。
ただ集計方法は、正直パワーで押し切っていました。画面からワークフロー結果をコピペし、スプレッドシートに記載して計算していました。 MCPを使って上手く集計できないか試したんですが、当時は適切なツールが無く(自分で開発出来ず)実現はできませんでした。
ただ今回の記事を書くにあたって、gh
コマンドとClaude Codeを使ってみたんですが、これが大成功でした!
以下のようにプロンプトを入力し、期待した結果を得られました。
もし同じような操作を行う場合、選択肢の1つとして覚えていただけたらと思います。
(ちゃんと手作業で計算した結果とほぼ一致していました!)
ghコマンドを利用して、hogeリポジトリの「Parallel Tests」ワークフローの2025年n月分の成功時のみの平均実行時間を計算してください!全ブランチ対象で! ↓ 📊 2025年8月 「Parallel Tests」ワークフロー実行統計 全体サマリー - 平均実行時間: 14分47秒(886.93秒) 月間推移(3月→4月→5月→6月→7月→8月) - 平均時間: 10分40秒 → 12分5秒 → 14分26秒 → 12分5秒 → 13分41秒 → 14分47秒
社内へのナレッジ展開
月1回の技術発表の機会で発表してきました。
ラクスではJavaを使った商材が他にもあるので、何か参考になればと思い発表したのですが、後日他のプロダクトのエンジニアから直接声をかけられ、対応方法を聞かれました。 早速今回の対応が少しでも他チームに貢献できるチャンスがあると思うと、嬉しい限りです!
伸び代
最後に伸び代を書きます。
- キャッシュの有効活用
- 重複しているビルド資材のキャッシュ活用
- ジョブの再配置
- テスト実行時間に偏りが生じてきたため、均等に再分配
- セットアップの分離
- DBのセットアップ部分を1つにまとめ、各テストへ配布
- Self-Hosted Runnerの利用
- 費用面や速度面で、改善余地の可能性
発展的な改善案
- PRで変更があった部分だけテスト実行
- ここで「将来的な差分テストなどへの拡張性」が有効に機能する想定
@SpringBootTest
などのテストが遅くなる設定を減らす- 不要な場所でも使っているので、精査し削除していくことで早くなることを期待
まとめ
今回は、Gradle + GitHub Actionsを用いたCIのテスト実行時間短縮についてご紹介しました。
実施内容と成果:
- Gradleのカスタムタスクによるテストのグループ化とGitHub Actionsのmatrix機能を組み合わせ、テストを4並列で実行
- 実行時間を20分50秒から10分40秒へと約50%短縮を実現
- テスト数が約1,000件増加した現在でも、14分台で実行完了
特に重要だったポイント:
- 小さく早く試す - 複雑な最適化より、まず設定変更で実現できる並列化から着手
- 計測の正確性 - 成功したワークフローのみを対象に改善効果を測定
- 事前の調整 - 費用対効果を算出し、関係部署と事前に相談することでスムーズな導入
今後もキャッシュ活用や変更箇所のみのテスト実行など、さらなる改善の余地があります。
実際に高速開発には様々な要因が影響するため、今回のCIテスト実行時間の短縮ですぐ効果が出る訳ではないと思います。 しかし、CI完了待ち時間のコンテキストスイッチ減少やPRのフィードバック高速化など、間接的に貢献できていると考えています。
本記事が同様の課題を抱える、特にJavaやGitHub Actionsを利用しているチームの参考になれば幸いです。
それでは!