こんにちは!日々のCI/CDパイプラインの待ち時間に、コーヒーを淹れる暇すらない……あるいは、コーヒーを淹れすぎてお腹がタプタプになるほど待たされている、なんて悩んでいませんか?
大規模なプロダクト開発において、「テストが遅い」というのは開発チーム全体の生産性をじわじわと蝕むガンです。テストが15分もかかれば、開発者のフロー状態は途切れ、context switch(文脈の切り替え)が発生してバグの温床になります。
今回は、CI/CDツール「CircleCI」が持つ強力な武器の一つ、「テストの並列化(Test Splitting)」を極限まで最適化する裏技を、基礎から現場で使える実践テクニックまで、優しく紐解いていきましょう。これをマスターすれば、あなたのプロジェクトのテスト時間は劇的に短縮され、毎日の開発が驚くほど軽快になりますよ!
—
1. CircleCIのテスト並列化って、そもそも何をしているの?
初心者の方に向けて、まず「テストの並列化」のコンセプトを分かりやすく説明しますね。
例えば、あなたの手元に「100個のテストケース」があるとします。
これを1台の仮想マシン(VM)で順番に実行すると、単純計算で100個分の時間がかかりますよね。
ここでCircleCIのパラレリズム(Parallelism)という機能を使います。これは、同じ設定の仮想マシンを「同時に複数台(例えば4台)」立ち上げる機能です。
しかし、ただ4台立ち上げただけでは、どのマシンも同じ100個のテストを走らせてしまい、意味がありません。
そこで登場するのが、CircleCIの真骨頂である `circleci tests split` コマンドです。
このコマンドは、「100個のテストファイルを、4台の仮想マシンに綺麗に(均等に)割り振る」という魔法のような仕事をしてくれます。
- マシン①: テスト1〜25を担当
- マシン②: テスト26〜50を担当
- マシン③: テスト51〜75を担当
- マシン④: テスト76〜100を担当
これを同時に走らせれば、理論上の実行時間は「4分の1」になりますよね。これがテストの並列化の正体です。
—
2. 最速で動かす!基礎セットアップと「Hello World」的動作確認
百聞は一見にしかず。実際にCircleCIでテストを並列化する最小限の設定(`.circleci/config.yml`)を見てみましょう。ここでは、Node.jsのJestなどを想定したシンプルな例を挙げます。
version: 2.1
jobs:
test:
docker:
- image: cimg/node:18.16.0
# ① 仮想マシンを「4つ」並列で起動する宣言
parallelism: 4
steps:
- checkout
- run:
name: 依存関係のインストール
command: npm ci
- run:
name: テストの並列実行
command: |
# ② 実行すべきテストファイルを動的に取得する
TESTFILES=$(circleci tests glob “src//.test.js” | circleci tests split –split-by=timings)
# ③ 抽出されたファイルだけをテストランナーに渡して実行
npx jest $TESTFILES
workflows:
test-workflow:
jobs:
- test
この設定のポイント
1. `parallelism: 4`: これだけで、CircleCIはこのジョブを4つのコンテナで同時に実行してくれます。
2. `circleci tests glob`: 指定したパターンに一致するテストファイルのパスをリストアップします。
3. `circleci tests split`: ここが今回の主役です!リストアップされたファイルを、現在のコンテナのインデックス(何番目のコンテナか)に合わせて自動的に分割してくれます。
—
3. 【極限の最適化】`–split-by=timings` でボトルネックを消し去る
さて、ここからが本題の「現場で使えるプロの知見」です。
ただファイルを均等な「数(ファイル数)」で分割するだけでは、実は不十分なことが多いです。なぜなら、「テストファイルによって、実行時間が全く違うから」です。
例えば、以下のようなケースを想像してください。
- `user.test.js`: わずか 0.1秒 で終わる(モックを使った単体テスト)
- `checkout.test.js`: 30秒 かかる(DBや外部APIを叩く結合テスト)
これらを適当に分割してしまうと、あるマシンには軽いテストばかりが集まって一瞬で終わり、別のマシンには重いテストが集中してしまい、最後の1台が終わるのを他のマシンがボーッと待つ(=無駄な時間と課金が発生する)という悲劇が起きます。
ここで使うべきなのが、先ほどのコードにも出てきた `–split-by=timings` というオプションです。
タイミングベース分割の仕組み
CircleCIは、過去にテストを実行した際の時間(タイミングデータ)を自動的に記録・学習しています。`–split-by=timings` を指定すると、CircleCIは「各マシンのテスト実行時間が可能な限り均等(フラット)になるように」、重いテストと軽いテストをパズルのように組み合わせて自動配分してくれます。
これにより、すべての並列マシンが「ほぼ同時にゴールする」という、美しすぎる理想郷が完成します。
> 💡 先輩エンジニアからのアドバイス:
> 初回実行時や、新しく追加されたテストファイルには過去のタイミングデータがありません。その場合、CircleCIは自動的にファイルサイズ(バイト数)を代替指標にして分割してくれます。最初は「データがないから効果が出ない」と思われがちですが、数回CIを回せば過去データが蓄積され、驚異的な最適化精度を発揮し始めます。
—
4. さらに一歩進む:失敗したテストだけを賢く再実行する(Rerunning from Failed)
テストの並列化が進むと、「4つのうち、マシン③の1つのテストだけがコケた」という状況がよく発生します。
そんなとき、「全テストを最初からもう一度やり直す」のは、エンジニアの美学に反しますよね。 時間の無駄です。
CircleCIには、ワークフローのジョブ単位、あるいはテスト単位でのリトライ機構がありますが、テストの並列化と組み合わせる場合は、テストランナー側の機能やCircleCIのテスト結果レポート(JUnit XML形式)を活用するのが定石です。
例えば、Jestを使っている場合、失敗したテストだけを記録して再実行するフラグ(`–onlyFailed`)があります。これとCircleCIを組み合わせた賢いワークフローの構成案がこちらです。
- run:
name: テストの実行(失敗時は結果を保存)
command: |
TESTFILES=$(circleci tests glob “src//.test.js” | circleci tests split –split-by=timings)
npx jest $TESTFILES –reporter=default –reporter=jest-junit
environment:
JEST_JUNIT_OUTPUT_DIR: “test-results/jest/”
- store_test_results:
path: “test-results”
CircleCIに `store_test_results` でテスト結果(XML)をアップロードしておくと、万が一テストが失敗した際、CircleCIのWeb UI上で 「Rerun from failed(失敗した部分から再実行)」 をポチるだけで、コケたテストが含まれていたシャード(コンテナ)だけをピンポイントで再実行してくれます。
—
まとめ:CI/CDの最適化は、開発者の「自由時間」を創る
今回は、CircleCIのテスト並列化(Test Splitting)を究極まで最適化する裏技について解説しました。
- `parallelism` で仮想マシンを並列化する
- `circleci tests split –split-by=timings` で、過去の実行時間に基づいた完璧なロードバランスを実現する
- テスト結果を蓄積し、無駄のないスマートなワークフローを構築する
テストが速くなると、コードを書いてからPR(プルリクエスト)を出すまでのストレスが嘘のように消え去ります。「あ、テスト終わった?」ではなく、「もう終わったの!?」という感動を、ぜひあなたのチームでも味わってみてください。
これをマスターすれば、毎日の作業が劇的に楽になりますよ。
明日のCI/CDライフを、最高のものにアップデートしていきましょう!