テストの実行時間は、快適さの問題として語られがちです。しかし実態はもっと深刻で、遅いテストは使われなくなります。手元で流さなくなり、まとめて に任せるようになり、失敗しても後回しになる。最終的には「落ちているのが常態」になり、安全装置として機能しなくなります。
つまり速さは、テストが資産であり続けるための条件です。この回は、計測 → 原因特定 → 削減 → 分割 → 並列化 の順で扱います。この順序自体が重要で、いきなり並列化に飛ぶと費用だけが増えます。
1. まず何が遅いかを測る
「テストが遅い」という感想からは何も始まりません。どのテストが、どれだけ時間を使っているかを出すところから始めます。ほとんどのテストランナーには実行時間を報告する機能があります。
# 遅い順に 20 件を表示pytest --durations=20
# 実行時間つきで JUnit XML を出し、後段で集計するpytest --junitxml=report.xml出力を眺めると、たいていは少数のテストが全体の時間の大半を占めていることが分かります。全体を薄く改善するより、上位のいくつかを潰すほうが効きます。パフォーマンス改善の一般論と同じで、まず分布を見るのが先です。
# 自分たちの CI が出した成果物を読む前提。外部から受け取った XML を# 解析する場合は defusedxml を使う(XXE / 展開攻撃を避けるため)。import xml.etree.ElementTree as ET
root = ET.parse("report.xml").getroot()cases = [ (float(c.get("time", 0)), f'{c.get("classname")}::{c.get("name")}') for c in root.iter("testcase")]cases.sort(reverse=True)
total = sum(t for t, _ in cases)top = cases[:10]share = sum(t for t, _ in top) / total * 100 if total else 0
print(f"全 {len(cases)} 件 / 合計 {total:.1f} 秒")print(f"上位10件が占める割合: {share:.1f}%")for t, name in top: print(f" {t:6.2f}s {name}")「上位10件が全体の何割を占めるか」がそのまま改善余地の大きさになります。ここが高いほど、少ない労力で短縮できます。
2. 遅さの原因は本数ではなく重さ
テストが遅いと聞くと本数を減らす発想になりますが、実際に支配的なのは1本あたりの重さであることが多いです。原因は概ね次のどれかに収まります。
| 原因 | 症状 | 対処 |
|---|---|---|
| 毎回コンテナやDBを起動 | 起動時間が本数分かかる | 起動を共有し、データだけ初期化する |
| 固定の待ち時間(sleep) | 常に一定時間ぶん遅い | 条件が満たされるまで待つ方式に変える |
| 外部への実通信 | 遅く、かつ不安定 | 境界を に置き換える |
| の作りすぎ | 使わないデータまで毎回生成 | テストごとに必要最小限にする |
| 全件を対象にした重い前処理 | 件数に比例して伸びる | 代表的な少数データで足りるか見直す |
`sleep(3)` のような固定待ちは、遅いだけでなく の温床でもあります。環境が遅ければ3秒でも足りず、速ければ完全な無駄です。条件が満たされるまで短い間隔で確認する方式に置き換えると、速くなり、かつ安定します。
3. 層で分けて、流す頻度を変える
すべてのテストを常に全部流す必要はありません。速い層は頻繁に、遅い層はまれにという設計にすると、体感の待ち時間は大きく下がります。
| 層 | 内容 | いつ流すか |
|---|---|---|
| 高速層 | 中心。外部依存なし | 保存のたび / 手元で常時 |
| 中速層 | 。DBあり | プルリクエストごと |
| 低速層 | 。主要導線のみ | マージ前 / 定期実行 |
| 確認層 | デプロイ直後 |
この分割で重要なのは、高速層だけで大半の問題が捕まる状態を作ることです。高速層が薄いと、結局は遅い層を回さないと不安になり、分割した意味がなくなります。EP.1 で扱った の話がここに効いてきます。
4. 変更範囲で絞り込む
もう一段効くのが、変更されたコードに関係するテストだけを流すというやり方です。多くのテストランナーやビルドツールが依存関係を追跡する機能を持っています。
ただしこれには前提があります。依存関係を正しく把握できていることです。動的な読み込みや設定ファイル経由の結合があると、関係するのに検出されないテストが出ます。取りこぼしが致命傷になる経路(決済など)は、絞り込みの対象から外して常に流すという運用にしておくと安全です。
変更範囲での絞り込みは、手元での反復には非常に有効です。一方でマージ前の最終確認では、絞り込みによる取りこぼしのリスクのほうが大きくなります。場面によって使い分けるのが現実的です。
5. 最後に並列化する
ここまでやってなお遅ければ、並列化します。順序を逆にしてはいけません。無駄を残したまま並列化すると、無駄が並列に実行されるだけで、時間は縮んでも費用は増えます。CI 上では実行時間がそのまま課金に効くため、この違いは実際の金額として現れます。
並列化の障害になるのは、ほぼ例外なく共有状態です。同じDBの同じテーブル、同じ一時ディレクトリ、同じ固定ポート。並列化を検討する時点で、テストが互いに独立しているかを確認する必要があります。
- データを共有しない — テストごとにスキーマや接頭辞を分ける
- 順序に依存しない — 前のテストが作ったデータを前提にしない
- 固定の資源を使わない — ポートやファイル名は動的に決める
- 時刻に依存しない — 現在時刻は差し替え可能にしておく
これらは並列化のためだけの作法ではなく、フレーキーテスト を減らす作法とほぼ同じです。並列化の準備をすると、副作用として安定性も上がります。逆に、並列化して急に落ち始めたテストは、もともと独立していなかったことが露呈しただけ、と考えるのが正しい見方です。
6. 速さと網羅のトレードオフをどう置くか
速さを追求すると、どこかで網羅性を削ることになります。ここは費用対効果で決める話で、一般解はありません。判断の材料になるのは、壊れたときに誰がいつ気づくかです。
デプロイ後すぐに人が触る画面なら、多少取りこぼしても人が気づきます。一方、夜間バッチの集計結果は誰も見ていないので、遅くても事前に検証しておく価値が高い。速さを削ってよい場所と、削ってはいけない場所は、この観点で分かれます。データ基盤側の事故については データ基盤トラブル事件簿 に具体例があります。
計測 → 原因特定 → 削減 → 分割 → 並列化 の順で進める。遅さの原因は本数ではなく1本あたりの重さであることが多い。並列化は最後で、その準備は フレーキーテスト 対策と重なる。次回は、最も遅く壊れやすい E2Eテスト の線引きを扱います。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。