CIが遅すぎて誰も見なくなった話|実行時間を短縮する7つの対策

「CIが終わるまで待てない」「どうせ通るだろう」——CI実行時間が長すぎることで生まれる「誰も待たない・誰も見ない」文化と、実行時間を短縮する7つの具体策を解説します。pytest-xdist並列化・Stage分割・キャッシュ設計・モック化など、実務で即使えるCI高速化の方法を紹介します。

CIは「壊れたとき」より「遅すぎるとき」の方が、じわじわとチームを壊します。「30分待って赤いCI」と「3分で赤いCI」では、同じ失敗でもチームへの影響が全く違います。

📌 この記事はこんな方におすすめ

  • CIの実行時間が10分を超えていて、チームが待つのを諦めている
  • 「PRを出してからCIを待たずにマージする」が習慣になっている
  • GitHub ActionsのCI高速化を具体的な方法で知りたいQAエンジニア
  • テストスイートが増えるたびにCIが遅くなる問題を根本から解決したい方

✅ この記事を読むと得られること

  • CIが遅くなる原因の種類と、それぞれが「誰も見なくなる」につながる仕組みがわかる
  • pytest-xdist・Stage分割・キャッシュ・モック化など、即実装できる高速化策がわかる
  • 「崩壊する前に対処する」ための早期警戒チェックリストがわかる

👤 この記事を書いた人

QAエンジニア・テスト自動化エンジニアとして15年以上の実務経験を持つ Yoshi が執筆。「CIを誰も待たなくなった」プロジェクトを複数経験し、E2E偏重・並列化なし・キャッシュなしの状態から実行時間を大幅短縮した実績をもとに解説します。

📖 関連記事との使い分け

📌 結論(3つのポイント)

  • CI高速化の優先度は ①並列化 ②Stage分割 ③キャッシュ ④モック化 ⑤テスト棚卸し の順が実務的に効果が大きい
  • 「CIを誰も待たなくなる」文化は、遅くなり始めてから3〜6ヶ月で定着する。予兆の段階で対処できる
  • CI実行時間の目標は 理想は「5分以内で最初のフィードバックが返る状態」。10分を超えたら改善を検討するケースが多い

「CI終わった?」「あ、まだ走ってる。とりあえずマージしとくか」——この会話が一度でも起きたなら、CIの速度問題は既に文化問題になっています。

Flakyテストと違い、CI速度の問題は「テストが落ちる」わけではありません。静かに、じわじわと、「CIを確認する文化」を侵食していきます。この記事では、CI遅延が引き起こす崩壊プロセスと、具体的な高速化策を解説します。

CI速度の問題が「誰も見なくなる」につながるプロセスとは?

段階チームの行動実行時間の目安
① 許容「少し長いけど待てる」〜5分
② 離脱「CI見ながら別の作業をする」5〜10分
③ 無視「CI待たずにマージする」が常態化10〜20分
④ 崩壊「CIが赤くてもとりあえずマージ」20分超
⚠️ 「誰も見なくなる」が最も危険な理由:CIが赤いままマージが続くと、本番環境での障害発生リスクが高まります。さらに「CIを確認しない文化」が定着すると、速度問題を解消してもチームの習慣は簡単には戻りません。遅くなる速度より、文化が壊れる速度の方が速いのです。

CIが遅くなる原因とは?7つの分類

「なんとなく遅い」ままにしておくと永遠に改善されません。まずどの原因で遅いかを特定することが高速化の第一歩です。

原因典型的な症状速度インパクト
① E2E偏重「テストは全部E2Eで書く」方針になっている🔴 大
② 並列化なしテストを逐次実行(1スレッドで順番に実行)🔴 大
③ キャッシュなし毎回 pip install・npm install を最初から実行🔴 大
④ 外部I/O依存テストが実際のDB・外部APIを使っている🟡 中
⑤ Stage設計なし全テストを1ジョブで一括実行している🟡 中
⑥ 不要なsleeptime.sleep(3)が各テストに散在している🟡 中
⑦ テスト肥大化古い・重複したテストが整理されていない🟢 小〜中

対策①:pytest-xdistで並列実行する

最も即効性が高い対策です。pytest-xdistを使うと、テストを複数のCPUコアで並列実行できます。テスト数が多いほど効果が大きく、100件以上のテストスイートでは実行時間が2〜4倍短縮されることが多いです。

# インストール
# pip install pytest-xdist

# ローカル:CPUコア数を自動検出して並列実行
pytest tests/ -n auto

# GitHub Actions:ワーカー数を明示的に指定
pytest tests/ -n 4

# pytest.ini で常時有効化(CI環境向け)
# [pytest]
# addopts = -n auto
#
# ⚠️ ローカルでの注意:-n auto を addopts に常時設定すると
# VSCode Test Explorer・デバッグ実行・coverage計測・Flaky調査
# と相性問題が出ることがあります。
# CI限定で有効化する場合は pytest.ini ではなく GitHub Actions の
# run: pytest tests/ -n auto として設定する方が安全です。
⚠️ pytest-xdistを使う前の注意点:並列実行はテストの独立性が前提です。テスト間でDB・ファイル・グローバル状態を共有していると、並列化によってデータ競合が発生してFlakyになります。並列化前に pytest --randomly-seed=1234(※pip install pytest-randomlyが必要)でテスト順序をシャッフルして実行し、順序に依存していないかを確認しましょう。また scope="session" の fixture との相性にも注意が必要です。
💡 実務Tip:E2Eテスト(Playwright/Selenium)を並列化する場合は、ブラウザインスタンスがテストごとに独立していることを確認してください。scope="function"のdriverフィクスチャを使っていれば基本的に並列化可能です。一方、状態変更を伴うscope="session"のfixture(ログイン状態やDB書き込みを伴うもの)は並列実行と相性が悪く、ワーカー間で状態が混在してFlakyになります。読み取り専用データやimmutableな設定値のfixture(DBの接続設定など)はscope="session"でも安全です。

対策②:CIをStageに分割して早期フィードバックを得る

「全テストが終わらないと何も分からない」設計では、どんなに速くしても限界があります。単体テスト→結合テスト→E2Eテストの順にStageを分けると、バグを早い段階で検出できます。

# GitHub Actions の Stage分割例(.github/workflows/test.yml)
name: CI Pipeline

on: [push, pull_request]

jobs:
  # Stage 1:単体テスト(目標:1分以内)
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run unit tests
        run: pytest tests/unit/ -n auto --tb=short

  # Stage 2:結合テスト(目標:3分以内)
  # 単体テストが通過した場合のみ実行
  integration-tests:
    needs: unit-tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run integration tests
        run: pytest tests/integration/ -n auto

  # Stage 3:E2Eテスト(目標:5分以内)
  # 結合テストが通過した場合のみ実行
  e2e-tests:
    needs: integration-tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Install Playwright browsers
        run: playwright install chromium --with-deps
      - name: Run E2E tests
        run: pytest tests/e2e/ -n 2
💡 Stage分割の効果:単体テストが1分で失敗すれば、E2Eの15分を待たずに開発者はフィードバックを受け取れます。「問題を早く知れる」設計が、CIを「待つもの」から「頼れるもの」に変えます。

対策③:依存関係キャッシュで毎回のインストールを省略する

毎回 pip install を実行していると、それだけで2〜5分かかることがあります。依存関係が変わっていない場合はキャッシュを利用することで、インストール時間をほぼゼロにできます。

# GitHub Actions でのキャッシュ設定例

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # ✅ Python依存関係のキャッシュ
      - name: Cache pip packages
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          # requirements.txtが変わったときだけキャッシュを更新
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - name: Install dependencies
        run: pip install -r requirements.txt

      # ✅ Playwrightのブラウザバイナリキャッシュ
      - name: Cache Playwright browsers
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ hashFiles('requirements.txt') }}

      - name: Install Playwright browsers (キャッシュがない場合のみ)
        run: playwright install chromium --with-deps
💡 キャッシュのキー設計:hashFiles('requirements.txt') を使うと、requirements.txtが変更されたときだけキャッシュが無効化されます。Playwrightブラウザのキャッシュは特に効果が大きく、初回インストール(2〜3分)をほぼ毎回省略できます。なお actions/setup-python@v5 には cache: "pip" オプションが内蔵されており、actions/cache を別途追加せずに済む場合もあります。

- uses: actions/setup-python@v5
  with:
    python-version: "3.11"
    cache: "pip"  # これだけでpipキャッシュが有効になる

対策④:外部I/OをモックしてCI環境の待機時間を削減する

実際のDB・外部API・メール送信などを使うテストは、ネットワーク遅延・外部サービスの応答時間がCIのボトルネックになることがあります。

from unittest.mock import patch, MagicMock
import requests
import pytest

from sqlalchemy import create_engine
from sqlalchemy.orm import Session, declarative_base

# Base の定義例(実際のプロジェクトではモデルファイルで宣言済みの想定)
# from sqlalchemy.orm import declarative_base
# Base = declarative_base()
Base = declarative_base()

# ❌ 遅い:実際の外部APIを毎回呼び出す
def test_send_notification():
    result = requests.post(
        "https://api.slack.com/api/chat.postMessage",
        json={"channel": "#test", "text": "テスト完了"}
    )
    assert result.status_code == 200
    # ネットワーク往復で 200〜500ms かかる

# ✅ 速い:外部APIをモックして即座に返す
@patch("requests.post")
def test_send_notification_fast(mock_post):
    mock_post.return_value = MagicMock(status_code=200)
    result = requests.post(
        "https://api.slack.com/api/chat.postMessage",
        json={"channel": "#test", "text": "テスト完了"}
    )
    assert result.status_code == 200
    # モックなので 1ms 以下で完了

# ✅ DBもフィクスチャでインメモリを使う
@pytest.fixture
def db_session():
    """SQLiteのインメモリDBを使った高速なDBフィクスチャ"""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        yield session
    # withブロックを抜けると自動でclose・rollback

対策⑤:time.sleepを排除して明示的待機に置き換える

100件のテストに time.sleep(2) が1つずつあると、それだけで3分超の固定ロスになります。sleepはFlakyの原因でもあり、CI高速化の観点からも真っ先に排除すべき対象です。

# time.sleep の使用箇所を一括検索(asyncio.sleep等との混同を避けるため明示的に指定)
# grep -R "time.sleep(" ./tests

# ❌ 遅い:固定待機
import time
time.sleep(3)
driver.find_element(By.ID, "result").click()

# ✅ 速い:条件が満たされたら即座に次へ
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, "result")))
element.click()
# 要素がクリック可能になった瞬間に実行(平均0.3秒、sleepより大幅短縮)

対策⑥:実行時間の長いテストを可視化して棚卸しする

「なんとなく全部必要」で追加し続けたテストが、CIを圧迫していることがあります。実行時間のプロファイリングで「遅いテスト上位10件」を可視化するだけで、改善の優先順位が明確になります。

# pytest 標準機能で実行時間の長いテストを表示(追加プラグイン不要)
pytest tests/ --durations=20

# 実務推奨:1秒以上かかるテストだけを表示(短いテストのノイズを除去)
pytest tests/ --durations=20 --durations-min=1.0

# 出力例:
# 15.23s call     tests/e2e/test_checkout.py::test_full_purchase_flow
#  8.91s call     tests/e2e/test_login.py::test_login_with_mfa
#  6.44s call     tests/integration/test_report.py::test_generate_pdf_report
#  3.12s call     tests/unit/test_user.py::test_create_user  ← 単体テストで3秒は要調査

# ❌ 問題のあるパターン:
# - 単体テストが3秒以上かかっている → 外部I/Oが混入している可能性
# - E2Eテストが20秒以上かかっている → テストのスコープが広すぎる可能性
# - 「同じシナリオを微妙に変えただけ」のテストが多数ある → parametrizeで統合を検討
💡 棚卸しの判断基準:以下のいずれかに当てはまるテストは削除・統合・移動を検討します。①単体テストで3秒以上かかる(外部I/O混入の疑い)、②E2Eテストで30秒以上かかる(スコープを分割できないか検討)、③6ヶ月以上失敗も修正もされていない、④他のテストとシナリオがほぼ重複している。

対策⑦:テストピラミッドを見直してE2E偏重を是正する

CIが根本的に遅い場合、原因はしばしば「テストピラミッドの逆転」にあります。E2Eテストばかり増えて、単体・結合テストが少ない状態がこれです。

テスト種類理想の比率実行時間の目安カバーするもの
単体テスト70%〜0.1秒/件関数・クラスの単体動作
結合テスト20%〜1秒/件モジュール間の連携・API
E2Eテスト10%10〜30秒/件主要なユーザーシナリオ
⚠️ E2Eで検証しなくていいものを見直す:以下はE2Eではなく単体・結合テストで検証できます。①バリデーションロジック(必須入力・文字数制限)→単体テスト、②APIのレスポンス形式・ステータスコード→APIテスト(requests/Playwright API)、③エラーメッセージの表示条件→結合テスト。E2Eは「ユーザーが実際に使う主要なシナリオ」だけに絞るのが理想です。

「誰も見なくなる前に」対処する予兆チェックリストとは?

以下のチェックリストで現状を確認してください。3つ以上あてはまったら、今すぐ改善を始めるサインです。

チェック項目該当?
CIの実行時間が10分を超えている✅ / ❌
PRのマージをCI完了前に行うことがある✅ / ❌
テストを逐次実行(並列化していない)✅ / ❌
毎回 pip install / npm install を最初から実行している✅ / ❌
テストコードに time.sleep() が10箇所以上ある✅ / ❌
E2Eテストが全テストの50%以上を占めている✅ / ❌
「テストを全部終わらせないと次が動かない」1ジョブ設計になっている✅ / ❌

🚀 今すぐ始める優先順位TOP3

1位pytest-xdist で並列化(今日できる・効果が最大)
pip install pytest-xdistpytest -n auto
2位キャッシュ設定を追加する(1〜2時間で完了)
pip/npm/Playwrightブラウザのキャッシュを GitHub Actions に追加
3位time.sleep の使用箇所を grep で洗い出す
grep -r "time.sleep" ./tests で全件確認し、上位から置き換える

FAQ

Q. CI実行時間の目標値はどれくらいが理想ですか?

一般的な経験則として、多くの現場では5分前後を超えると待機率が下がり始める傾向があります。10分を超えると離脱率が上がり始め、20分超は「誰も待たない」文化が定着するリスクが高まります。プロジェクト規模やチームの働き方によって許容範囲は変わりますが、まず現状の実行時間を計測し、「今より30%短縮する」という目標から始めるのが現実的です。

Q. pytest-xdistを使うとテストが不安定になるのですが?

並列化でFlakyが増える場合、ほとんどの原因はテスト間のデータ共有または実行順序への依存です。pytest --randomly-seed=random で順序依存を確認し、テストデータはuuidなどでユニーク化してください。またscope="session"のfixtureは並列実行と相性が悪いことがあるため、scope="function"への変更を検討してください。

Q. E2Eテストだけで200件あります。全部並列化できますか?

テストが独立している(共有DBやグローバル状態を使っていない)なら並列化できます。ただしE2Eテスト200件はそれ自体が問題のサインです。「本当にE2Eで検証すべきシナリオか」を見直して、単体・APIテストで代替できるものを移動させると、並列化と合わせて大幅な短縮が期待できます。

Q. GitHub Actions のキャッシュはどれくらい効果がありますか?

依存関係が変わらない場合、pip installの2〜5分とPlaywrightブラウザインストールの1〜3分が省略できます。合計で毎回3〜8分短縮できることが多く、費用対効果が最も高い改善のひとつです。ただしキャッシュのkey設計を誤ると古い依存関係が使われてバグを見逃すリスクがあるため、hashFilesでrequirements.txtの変更を確実に検知する設計が重要です。

CIは「正しいコードを守るためのもの」ですが、遅すぎると「誰も見ないもの」になります。技術的な問題のはずなのに、放置すると文化的な問題に変わっていきます。今日の実行時間を計測して、まず1つだけ改善してみてください。「5分のCIを待てるチーム」と「30分のCIを諦めたチーム」では、1年後の品質に大きな差が生まれます。

📋 この記事のまとめ

  • CI速度の問題は「文化崩壊」を引き起こす。多くの現場では、CIを待たない習慣が数ヶ月単位で定着し始め、放置期間が長いほど習慣化しやすい。10分超えたら要改善・20分超えたら緊急対応のサイン
  • 対策の優先順位:①pytest-xdist並列化(効果最大・今日できる)②キャッシュ設定(3〜8分短縮)③Stage分割(早期フィードバック)
  • time.sleepは grep -r "time.sleep" ./tests で洗い出してWebDriverWait/expect()に置き換える
  • E2E偏重はCI遅延の根本原因。単体テスト70%・結合テスト20%・E2E10%のピラミッドを目指す
  • pytest –durations=20 で遅いテスト上位を可視化し、定期的に棚卸しする
  • 並列化でFlakyが増えた場合はデータ競合が原因。ユニークデータ生成とscope見直しで解消
タイトルとURLをコピーしました