0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

5,500本のテストを4分で完走させる!PHPUnitを高速化するCircleCIの設定ポイント

0
Last updated at Posted at 2026-02-11

はじめに

プロジェクトが成長し、テストコードが増えるにつれてCIの実行時間は肥大化します。私のプロジェクトでは現在約5,500件のテストが存在しますが、様々な最適化を組み合わせることで、CircleCI上で約4分という実行環境を実現しています。

今回は、実際に効果があった最適化手法をまとめました。

DBマイグレーションを5倍速くする「tmpfs」マウント

CIにおいて最大のボトルネックになりやすいのがデータベース(MySQL)のI/Oです。
CircleCIのMachine Executorを利用し、MySQLのデータディレクトリを tmpfs(メモリ上の仮想ファイルシステム) にマウントすることで、ディスクI/Oを排除しました。

- run:
    name: Mount tmpfs for MySQL data
    command: |
        mkdir -p docker/volumes/mysql
        sudo mount -t tmpfs -o size=2G tmpfs docker/volumes/mysql

  • 効果: スキーマ作成やダンプのインポートといった処理が、通常のファイルシステムと比較して約5倍高速化されました

Xdebugの排除

デバッグやカバレッジ計測に便利なXdebugですが、有効なだけでPHPの実行速度は大幅に低下します。CI環境ではデバッグ機能は不要なため、コンテナ起動時に設定ファイルからExtensionを削除しています。

  • 効果: テスト実行時間が約2倍速くなりました

ParaTest × CPU数に応じた並列DB生成

並列テスト実行ライブラリ ParaTest を使用していますが、マシンのスペックをフルに活かすため、CPU数(nproc)を動的に取得し、その数だけ専用のテスト用DBを並列でセットアップします。

CPU_COUNT=$(nproc)
while (( CPU_COUNT > 0)); do
  # CPUの数だけ並列でDB作成や権限付与を実行
  docker compose exec -T db mysql -u root -p"${DATABASE_PASSWORD}" -e "grant all on \`test_db"${CPU_COUNT}"\`.* to 'user';" &
  pids[CPU_COUNT]=$!
  CPU_COUNT=$((CPU_COUNT - 1))
done
wait "${pids[@]}"

リソースの競合を避けつつ、ハードウェアの限界まで並列度を高めることが可能です。また、実行時間に応じて柔軟に resource_class を変更できるようになります。

外部通信をiptablesで「即時拒否」する

テスト中に意図せず外部APIへリクエストが飛ぶと、タイムアウト待ちによる数秒〜数十秒のロスが発生します。これを防ぐため、iptables でコンテナからの外部ネットワーク通信を遮断しています。

# DOCKER-USERチェインのデフォルトRETURNを削除し、外部通信をブロックする
sudo iptables -D DOCKER-USER -j RETURN
# コンテナ間通信(プライベートネットワーク)は許可
sudo iptables -A DOCKER-USER -d 172.16.0.0/12 -j RETURN
sudo iptables -A DOCKER-USER -d 10.0.0.0/8 -j RETURN
sudo iptables -A DOCKER-USER -d 192.168.0.0/16 -j RETURN
# TCP接続はRSTで即座に拒否。cURLなどが即エラーを返す
sudo iptables -A DOCKER-USER -p tcp -j REJECT --reject-with tcp-reset
# それ以外のプロトコルはICMPで拒否
sudo iptables -A DOCKER-USER -j REJECT --reject-with icmp-port-unreachable

単にドロップするのではなく tcp-reset を返すことで、クライアントがタイムアウトを待たずに即座にエラーを検知できるため、無駄な待ち時間がゼロになります。

DB接続の再確立を抑える StaticDriver の活用

dama/doctrine-test-bundleStaticDriver を導入しています。
通常、Symfonyのテストでは bootKernel() ごとにDB接続が再確立されますが、これを静的に保持することで接続コストを削減しています。

効果: テスト実行時間を約40%短縮できました。

初期スキーマの導入に schema:update を使わず「SQLダンプ」をインポートする

Doctrineを使用していると doctrine:schema:update --force でDBスキーマを構築したくなりますが、これはCI環境では非常に低速です。
本プロジェクトでは、あらかじめ書き出しておいた db_dump.sql を直接 MySQL に流し込む手法を採用しています。

# 1つずつマイグレーションを走らせるより、ダンプを流し込む方が圧倒的に速い
docker compose exec -T db mysql -u root -p"${DATABASE_PASSWORD}" app-test"${CPU_COUNT}" < db_dump.sql &

Dockerのボリュームをそのままキャッシュ(バックアップ)して使い回す方法も検討しましたが、環境依存による不安定さやキャッシュの肥大化、リストアにかかる時間といった課題があるため 「クリーンな状態からのSQLインポート」 という、速度と安定性のバランスが最も良い着地点を選びました。

なぜ「DBセットアップ」の高速化にこだわるのか?

PHPUnit(ParaTest)によるテスト実行部分は、CPUコア数を増やせば増やすほど線形に速度が向上します(レバレッジが効く)。
しかし、DBの初期化やマイグレーションといった「前準備」の時間は、並列度を上げても短縮できません。

  • 並列度を上げるほど、セットアップ時間がCI全体のボトルネック(支配的)になる
  • 前準備を1分縮めることは、テスト実行部分を1分縮めることよりも価値が高い

そのため、前述の tmpfsマウントSQLインポート によって「逐次実行される前準備」を極限まで削ぎ落とすことが、ハイスペックなCIマシンの性能をフルに引き出す鍵となります。

「AI時代の高速道路」を守る、独自のスローテスト検知

CIを一度高速化しても、日々の開発で「遅いテスト」が紛れ込めば、すぐに元の木阿弥です。特にAIによるコード生成が標準的になった現代では、人間が書くよりも遥かに容易に、 「意図せず大量のエンティティを生成するテスト」「重いクエリを連発するテスト」 が爆速で量産されるリスクがあります。

こうした「AI時代の副作用」からCIの速度を守るため、独自の SlowTestFailListener を導入しています。

なぜ独自Listenerが必要なのか?

PHPUnit自体にも遅いテストを検知して Risky 判定にする仕組みはありますが、ParaTestを導入している環境では、少なくとも今利用しているバージョンでは、Risky判定になってもCIが正常終了(Pass)してしまうという問題があります。

そのため、独自Listenerで制限時間を超えたテストを強制的に Failure (失敗)として扱うようにしています。

// SlowTestFailListener.php のエッセンス
public function endTest(Test $test, float $time): void
{
    // CI環境以外(xdebug使用時など)はスキップ
    if (!getenv('CI')) return;

    // サイズごとの制限時間
    $limit = self::TIME_LIMITS[$test->getSize()] ?? self::DEFAULT_TIME_LIMIT;

    if ($time > $limit) {
        $test->getTestResultObject()->addFailure(
            $test,
            new AssertionFailedError("テストが {$time}秒かかりました(制限: {$limit}秒)。..."),
            $time
        );
    }
}

「AI時代のための高速道路」の整備

開発者がAIを使いこなし、凄まじいスピードで開発を進めるためには、CIという「高速道路」が常に舗装されている必要があります。

  • 早期検知: 実行時間が伸びた瞬間にCIを落とし、開発者にフィードバックする
  • アノテーションによる意図の明示: 本当に重いテストが必要な場合は @medium@large を付けるよう促す

このように「速く走れる仕組み」だけでなく、「遅くなる原因を自動で弾く仕組み」をセットで用意することが、AI時代のCI/CD戦略には不可欠です。

高速なCIは「最強の開発ツール」

CIを短時間で終わらせることは、単にサーバーコストを抑えることではありません。開発者が「このコードをプッシュしても大丈夫か?」と不安になる時間を最小化し、 試行錯誤のサイクルを高速化させるための、最も投資対効果の高いエンジニアリング です。

PHPUnitの実行時間が課題になっているプロジェクトにとって、これらの手法のどれか一つでも解決のヒントになれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?