アプリケーションのテストやCI/CDは、開発の品質を支える工程です。テストが増えると、CIの結果待ちがボトルネックになります。
テストを減らせないケースでは、並列実行で1回あたりの時間を短くすることで対処することになります。
本記事ではCircleCIが標準でサポートする並列実行の方法である、タイミングベースの静的分割(static test splitting)について紹介します。CircleCIでのテスト分割の特徴は、テストコードや実行コマンドを大きく変更する必要がないことです。
この記事ではVitestを例に紹介します。ほかのテストツールの例について知りたい場合は、記事へのコメントで教えてください。
Vitestでのテストファイル列挙とJUnit生成をテストする
まずVitestを実行しているリポジトリで、JUnit XMLが生成できるかを確認してください。vitestでは--reporter=junitを使います。
% npx vitest run --reporter=junit
<?xml version="1.0" encoding="UTF-8" ?>
<testsuites name="vitest tests" tests="2" failures="0" errors="0" time="0.01054925">
<testsuite name="src/index.test.ts" timestamp="2026-09-25T07:07:34.330Z" hostname="cci-hidetakaokamoto-l4379" tests="2" failures="0" errors="0" skipped="0" time="0.01054925">
<testcase classname="src/index.test.ts" name="GET / > returns Hello Hono!" time="0.008839416">
</testcase>
<testcase classname="src/index.test.ts" name="GET /time > returns current time as ISO 8601 JSON" time="0.001157292">
</testcase>
</testsuite>
</testsuites>
続いてvitest list --filesOnlyを実行します。このコマンドで、Vitestがどのテストファイルを実行するかの一覧が出ます。この2つのコマンドをあとでCI設定に使うので、エラーや無関係なログが混ざっていないかを確認してください。
% npx vitest list --filesOnly
src/index.test.ts
test-suites.ymlを作り、ローカルで検証する
次に.circleci/test-suites.ymlを作成します。runにテストコマンド、discoverにatom列挙、outputs.junitにJUnitの出力先を書きます。
---
name: ci tests
discover: npx vitest list --filesOnly
run: npx vitest run --reporter=junit --outputFile="<< outputs.junit >>" --bail 0 << test.atoms >>
outputs:
junit: test-reports/junit.xml
discoverはテスト対象の一覧だけを出力します。無関係なパスが混ざると、意図しないテスト実行の原因になるので注意してください。
runは各並列ノードで走らせるコマンドです。割り当てられたテストファイル名などは、次のプレースホルダで渡します。
-
<< test.atoms >>… そのノードに割り当てられたテストファイル(スペース区切り) -
<< outputs.junit >>… JUnit XMLの出力パス(outputs.junitと対応)
name・discover・run・outputsなど各キーの詳細については、testsuite configuration referenceで確認できます。
CircleCI CLI testsuite拡張を追加する
CircleCI CLIを最新にし、circleci extension install testsuiteで拡張を入れてください。バージョンはcircleci versionで確認できます。入れ方はThe CircleCI CLIを参照してください。CIコンテナ側の追加インストールは不要です。
その後、circleci testsuite doctorを実行します。
circleci testsuite doctor "ci tests"
doctorの必須チェックがすべてsuccessで、action itemsが空であることを確認してください。問題なければrunで、ローカルから同じコマンド経路を一度通します。
circleci testsuite run "ci tests"
これで、.circleci/test-suites.ymlのテストコマンドがローカルでも問題なく動くことを確認できました。
config.ymlにparallelismと testsuite を組み込む
次は定義したテストコマンドを、CIサービス上でのテスト実行ステップに追加しましょう。
circleci testsuite runをステップに追加し、parallelismを2以上にしてください。store_test_resultsのpathは、.circleci/test-suites.ymlのoutputs.junitに合わせます。パスがずれると、次回以降のタイミングデータが蓄積されません。
version: 2.1
jobs:
test:
docker:
- image: cimg/node:22.22
parallelism: 4
steps:
- checkout
- run:
name: Install dependencies
command: npm ci
+ - run:
+ name: Run testsuite
+ command: circleci testsuite run "ci tests"
- store_test_results:
path: test-reports
workflows:
version: 2
ci-tests:
jobs:
- test
parallelismの取りうる値や制約は、Configuration Reference の parallelismに書いてあります。
CI で分割されたことを確認する
push後は、実行結果やログを確認しましょう。
1 回目: 件数と「タイミング無し」
CircleCIダッシュボードでジョブを開き、Tests タブの件数がローカル全件と一致するかを見てください。
ログではPrimaryノードがdiscoverし、他ノードはWaiting for tests...と出ます。タイミングデータがまだないテストについては、次のようなログです。
Autodetected classname timings for 1 tests.
No timing data (testname, classname or filename) found for 8 tests:
初回はタイミング履歴が少ないため、件数寄りの割り振りになります。ノード間の所要時間にも偏りが出やすく、並列化の効果はまだ限定的です。
2 回目: 新コミットで timing を見る
成功したジョブの Rerun(同じコミットの再実行)では、次のログとともに成功済みのtest atomがスキップされます。失敗したテストだけを再実行する動作です。
Detected rerun, skipping previously successful test atoms
Selected 0 test atoms, Skipped 9 test atoms in 0s
Testsはすべてskippedになり、タイミングベース分割の観察には使えません。
設定を変えずに新しいコミットを pushしてフル実行してください。タイミング蓄積後は、次のようなログが出力されます。
Autodetected classname timings for 9 tests.
遅いテストは単独ノードへ、速いテストは他ノードへと振り分けられます。
ダッシュボードの Timings タブで、ノードごとの所要時間の偏りが1回目より小さいかを確認してください。
まとめ
Vitestのリポジトリなら、.circleci/test-suites.ymlにdiscover・run・outputsを書き、config.ymlでparallelismとcircleci testsuite runを足せば、タイミングベースの静的分割を始められます。ローカルでは拡張を入れたうえでdoctorとrunを通してください。CIではstore_test_resultsのパスをoutputs.junitに合わせます。
初回はタイミングが少なく、件数寄りの配分です。偏りが小さくなるのは、新しいコミットで実行した2回目以降に限られます。同じコミットのRerunは成功済みをスキップするため、timingの確認には使えません。
今回の設定をベースに、pushしたコードの差分と関係のあるテストだけを実行する「CircleCI Smarter Testing」を設定できます。詳細は以下の日本語ブログをご確認ください。
関連リンク





