「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偏重・並列化なし・キャッシュなしの状態から実行時間を大幅短縮した実績をもとに解説します。
📖 関連記事との使い分け
- Flakyテスト地獄から脱出する方法:CIへの信頼崩壊が「Flaky起因」の場合 → テスト結果が不安定な方はこちら
- GitHub Actions × Playwright でCI構築:CI設定の「方法」 → CI自体の設定方法を知りたい方はこちら
- この記事:CIが「遅すぎる」ことで起きる文化崩壊と、高速化の7つの具体策
📌 結論(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が遅くなる原因とは?7つの分類
「なんとなく遅い」ままにしておくと永遠に改善されません。まずどの原因で遅いかを特定することが高速化の第一歩です。
| 原因 | 典型的な症状 | 速度インパクト |
|---|---|---|
| ① E2E偏重 | 「テストは全部E2Eで書く」方針になっている | 🔴 大 |
| ② 並列化なし | テストを逐次実行(1スレッドで順番に実行) | 🔴 大 |
| ③ キャッシュなし | 毎回 pip install・npm install を最初から実行 | 🔴 大 |
| ④ 外部I/O依存 | テストが実際のDB・外部APIを使っている | 🟡 中 |
| ⑤ Stage設計なし | 全テストを1ジョブで一括実行している | 🟡 中 |
| ⑥ 不要なsleep | time.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 --randomly-seed=1234(※pip install pytest-randomlyが必要)でテスト順序をシャッフルして実行し、順序に依存していないかを確認しましょう。また scope="session" の fixture との相性にも注意が必要です。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対策③:依存関係キャッシュで毎回のインストールを省略する
毎回 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-depshashFiles('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で統合を検討対策⑦:テストピラミッドを見直してE2E偏重を是正する
CIが根本的に遅い場合、原因はしばしば「テストピラミッドの逆転」にあります。E2Eテストばかり増えて、単体・結合テストが少ない状態がこれです。
| テスト種類 | 理想の比率 | 実行時間の目安 | カバーするもの |
|---|---|---|---|
| 単体テスト | 70% | 〜0.1秒/件 | 関数・クラスの単体動作 |
| 結合テスト | 20% | 〜1秒/件 | モジュール間の連携・API |
| E2Eテスト | 10% | 10〜30秒/件 | 主要なユーザーシナリオ |
「誰も見なくなる前に」対処する予兆チェックリストとは?
以下のチェックリストで現状を確認してください。3つ以上あてはまったら、今すぐ改善を始めるサインです。
| チェック項目 | 該当? |
|---|---|
| CIの実行時間が10分を超えている | ✅ / ❌ |
| PRのマージをCI完了前に行うことがある | ✅ / ❌ |
| テストを逐次実行(並列化していない) | ✅ / ❌ |
| 毎回 pip install / npm install を最初から実行している | ✅ / ❌ |
テストコードに time.sleep() が10箇所以上ある | ✅ / ❌ |
| E2Eテストが全テストの50%以上を占めている | ✅ / ❌ |
| 「テストを全部終わらせないと次が動かない」1ジョブ設計になっている | ✅ / ❌ |
🚀 今すぐ始める優先順位TOP3
| 1位 | pytest-xdist で並列化(今日できる・効果が最大)pip install pytest-xdist → pytest -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/CD設定・高速化】
【テスト品質・設計】
- Flakyテスト地獄から脱出する方法|CI信頼性を取り戻す
- テスト自動化でよくある失敗5選|E2E偏重の落とし穴
- 自動化しない方がいいテストケース7選
- Page Object Model|保守コスト爆発を防ぐ設計パターン
【ロードマップ】
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見直しで解消
