Tech Learning Daily

2026-08-26 (Wed) — 第 41 号
AI が毎朝届ける、ソフトウェア技術の基礎解説
Dev 🌿 基礎 ⏱ 約 7 分

なぜ「単体テストを厚く、E2E を薄く」と言われるのか?

ブラウザを自動操作して画面をポチポチ確かめる E2E テストを書けば、バグは一番見つかりそうな気がする。実際にユーザーが触る経路をそのままなぞるのだから、これが一番「本物」の検証に思える。ところが多くのチームは E2E をあえて少数に絞り、地味な単体テストを大量に積む。なぜ一番リアルなテストを主役にしないのだろうか。

🎯 3 行まとめ
  • テストピラミッドは「単体テストを多数・高速に、統合テストを中程度に、E2E テストを少数・低速に」という比率でテストを構成する考え方だ
  • E2E は実行が遅く不安定(flaky)になりやすいうえ、失敗しても原因がアプリのどこにあるか特定しづらい。理由は本物のブラウザ・ネットワーク・DB を貫通させるからだ
  • ピラミッドを無視して E2E に偏ると「アイスクリームコーン」と呼ばれる逆三角形になり、CI が遅く壊れやすくなって開発速度そのものが落ちる

単体テスト・統合テスト・E2E テストは何が違うのか

単体テスト(unit test)は、関数やクラス 1 つなど小さな単位を、外部の DB やネットワークに触れずに検証するテストだ。純粋な計算ロジックであれば依存先を一切呼ばずに済むし、依存先があってもテストダブル(本物の代わりに使う偽物のオブジェクト。用途に応じて「スタブ」「モック」などいくつかの種類がある)で置き換えて切り離す。ソフトウェア設計者 Martin Fowler のブログ記事 The Practical Test Pyramid は、依存先を全てスタブに置き換えるテストを「solitary(孤立した)」、本物の協調オブジェクトを呼ぶテストを「sociable(社交的)」と呼び分けている。どちらも広い意味で単体テストであり、書き方に唯一の正解があるわけではない。

統合テスト(integration test)は、アプリケーションの外にある実際の部品――DB・メッセージキュー・他サービスの API など――と実際につないで検証する。同記事は「データをシリアライズ/デシリアライズする箇所は必ず統合テストで書け」と述べている。ORM のクエリが本当に意図した SQL を発行するか、JSON のパースが期待通りかは、モックに置き換えた単体テストでは検証できないからだ。

E2E テスト(end-to-end test)は、ブラウザや実機を自動操作し、ユーザーの操作からサーバー・DB までの経路をまるごと通して検証する。この 3 層を下から積んだとき「下ほど多く、上ほど少なく」という末広がりの三角形になるべきだ、というのがテストピラミッドの主張だ。2012 年ごろに Fowler が提唱したこの考え方は、コンサルタント Mike Cohn の著書に由来するとされる。

        ▲
       ╱E2E╲       少数・低速・高コスト
      ╱──────╲
     ╱ 統合   ╲    中程度
    ╱──────────╲
   ╱   単体      ╲  多数・高速・低コスト
  ╱──────────────╲
🍱 たとえるなら

車の出荷検査を思い浮かべてほしい。工場では部品 1 つ 1 つ――ネジの締めトルク、ブレーキパッドの厚み――を個別にすばやく測る検査を大量にこなす。壊れていれば秒単位で「このネジが不良」と特定できる。これに加えて、完成車を実際に路上で試走させる検査もするが、こちらは 1 台あたり時間がかかるし、何か異常が出ても「ブレーキか、ステアリングか、それとも路面のせいか」を切り分けるのに手間がかかる。だから試走は代表的な数台だけに絞り、大半の品質保証は部品単位の検査で先に済ませておく。

なぜ E2E は「少数」であるべきなのか

直感では「本物に近いテストほど信頼できるから、たくさん書くべき」と思えるかもしれない。しかし実際には、本物に近いことそのものが遅さと不安定さの原因になる。Google のテストエンジニア Mike Wacker は 2015 年の記事 Just Say No to More End-to-End Tests で、E2E テストが抱える 3 つの問題を指摘している。ひとつは実行時間で、ブラウザの起動・ページ遷移・実ネットワーク越しの通信を毎回待つため、テストの数が増えるほど CI 全体の所要時間が線形以上に伸びていく。もうひとつはflaky(不安定)――同じコードに対して実行するたびに成功したり失敗したりする状態――になりやすいことだ。実ブラウザの描画タイミングや実ネットワークの応答速度は環境ごとにばらつくため、コードは正しいのにテストだけが時々落ちる、という状況を生みやすい。3 つめは、失敗したときに原因を特定しにくいことだ。ログイン画面からカート・決済まで一続きで検証する E2E テストが落ちても、失敗箇所がフロントエンドのバグかバックエンドの API 変更かネットワークの一時的な問題か、テストの出力だけからは判別しづらい。

同記事はこの反省から、テストの規模の目安として「単体テストを 70%、統合テストを 20%、E2E テストを 10%」というおおよその比率を示している。数字自体を絶対視する必要はないが、「上に行くほど絞る」という方向性の目安として広く引用されている。

ピラミッドを無視するとどうなるか ― アイスクリームコーン

E2E テストは「画面を人が操作するのと同じ手順を再現するだけ」に見えるため、テストの書き方を知らないチームほど、まずここから手を付けてしまいやすい。単体テストを書くには対象のコードをテストしやすい形に設計し直す必要があることが多いが、E2E テストは既存の画面をそのまま外側から叩けるので、着手の心理的ハードルが低いからだ。その結果、単体・統合テストが少ないまま E2E だけが積み上がっていくと、ピラミッドが逆さまになった形になる。Fowler はこれを「アイスクリームコーン」と呼び、「保守の悪夢になりかねないので注意しろ」と警告している。E2E だらけの状態は、CI が遅くなるだけでなく、1 つの機能変更で無関係に見える大量のテストが同時に壊れ、どれが本当のバグでどれが単なるテストの脆さかを毎回人間が仕分けする羽目になる。

だからといって統合テストや E2E テストが不要というわけではない。Fowler は「上位のテストほど数を絞るべきだ」と述べる一方で、単体テストだけでは検証できない領域――複数コンポーネントをまたぐ結合や、実際のユーザー導線が壊れていないかの最終確認――には統合テストと E2E テストが必要だとも述べている。ピラミッドが主張しているのは「E2E をゼロにしろ」ではなく「テストの大部分は下の層で済ませ、上の層は本当に必要な代表的シナリオに絞れ」という配分の話だ。

💼 実務でどう出会うか

CI のジョブ一覧を見て「E2E だけ数分〜十数分かかっている」「E2E がたまに理由もなく落ちて re-run すれば通る」状態に心当たりがあれば、それはアイスクリームコーン化のサインだ。新しい機能を追加するとき、まず単体テストで分岐やエッジケースを網羅し、外部依存をまたぐ箇所だけ統合テストで確認し、E2E は「ログインしてから購入が完了する」のような代表的な 1〜2 本の導線に絞る、という順序で考えると配分を崩しにくい。

⌨️ 手を動かす(5 分)

「本物に近いテストほど遅い」という感覚を、実行時間の比較で確かめる。Python の標準ライブラリだけで、依存を持たない単体テストと、E2E が待つ「ネットワーク応答」を模した遅延を疑似的に再現する。

mkdir -p /tmp/test-pyramid-demo && cd /tmp/test-pyramid-demo
cat > test_demo.py <<'EOF'
import time
import unittest

def add(a, b):
    return a + b

class UnitTests(unittest.TestCase):
    def test_add_is_fast(self):
        self.assertEqual(add(2, 3), 5)

class SlowE2ELikeTest(unittest.TestCase):
    def test_full_checkout_flow(self):
        # 実際のE2Eはブラウザ起動・画面遷移・実ネットワーク応答を待つため遅い。
        # ここではその「待ち」を time.sleep で疑似的に再現する。
        time.sleep(0.5)
        self.assertTrue(True)

if __name__ == "__main__":
    unittest.main()
EOF
time python3 -m unittest test_demo -v

test_add_is_fast はミリ秒未満で終わるのに対し、test_full_checkout_flow だけで 0.5 秒かかるのが time の出力から見えるはずだ。これはあくまで sleep による疑似的な再現であり、実際の E2E はブラウザ起動やページ描画も加わってさらに遅い。それでも「テストの中身が本物の I/O に近づくほど、1 本あたりのコストが跳ね上がる」という核心は、この縮小版でも体感できる。同じ倍率のテストを 100 本 E2E で書けば、それだけで CI に 50 秒以上を追加で持っていかれる計算になる。

🙅 よくある誤解
  • E2E テストが一番信頼できるので、可能な限り E2E で書くべきだ — E2E は本物のブラウザ・ネットワーク・DB を貫通させる分だけ遅く flaky になりやすく、失敗時の原因特定にも時間がかかる。「実際のユーザー導線に近い」という価値と、「遅く不安定」というコストはトレードオフの関係にある。
  • モックを使ったテストは本物の動作を検証していないので意味がない — モックで外部依存を切り離すのは、対象コードのロジックだけを高速かつ安定して検証するためだ。外部との実際のやり取りが正しいかどうかは、統合テストという別の層が担当する役割分担になっている。
  • 単体テストの数を増やせば増やすほど品質は上がる — 数がいくら多くても、統合テストが検証すべき「複数コンポーネントをまたいだ結合」や、E2E が検証すべき「実際のユーザー導線」はカバーできない。層ごとに検証できる範囲が違うため、比率だけでなく各層の役割分担も必要になる。
📖 用語ミニ辞典
テストピラミッド
単体テストを多数・高速に、統合テストを中程度に、E2E テストを少数・低速に配分すべきだとする考え方。
単体テスト(unit test)
関数やクラスなど小さな単位を、多くの場合外部依存から切り離して検証するテスト。
統合テスト(integration test)
DB や外部 API など、アプリケーションの外にある実際の部品と接続して検証するテスト。
テストダブル
本物のオブジェクトの代わりに使う偽物のオブジェクトの総称。スタブやモックなどの種類がある。
flaky(不安定なテスト)
コードを変更していないのに、実行するたびに成功したり失敗したりするテスト。
アイスクリームコーン
E2E テストが単体・統合テストより多くなり、ピラミッドが逆さまになった保守困難な状態を指す比喩。
🔗 もっと深く