はじめに
朝、自分のプルリクに付いた赤いCIバッジを見て、何回目かの「またか」と思ったことはありませんか。
テストは増えた。自動化もした。
それでも本番では、小さな事故がなくならない。
CIを入れたのに品質が上がりきらないとき、原因はテストの本数や並列数だけではないかもしれません。
どのテストを、どのタイミングで、どの環境で動かすか。
その配置が曖昧なままだと、CIはだんだん「遅い」「赤い」「信用できない」ものになっていきます。
この記事では、CIを単なる「テストを走らせる場所」ではなく、変更に対して早く反応するための複数のループとして見ていきます。
TL;DR
- CIは単なるテスト実行装置ではなく、変更に対して早く反応するためのフィードバックループです
- 速くて安定したテストはコミット前に置き、遅いテストや本番に近い検証は後段に回します
- フレーキーテストが増えると、赤も緑も信用されなくなります。外乱を減らし、赤くなった後の動きまで決めておくことが重要です
1. CIには複数のループがある
CIを導入したのに、なぜ品質が上がりきらないのか。
多くの場合、原因は「CIを1つの自動テスト箱として見ている」点にあります。
まずは、CIの中に複数のループがあるものとして見ていきます。
1-1. デグレを「個人の注意力」ではなく「構造」で防ぐ
変更には、どうしても壊れる可能性がつきまといます。
どんなに優秀な人でも、自分の変更が想定外の場所に影響を与えることはあります。
これを「変更時にちゃんと動作確認しよう」という個人の注意力で受け止めようとすると、必ずどこかで漏れます。
構造で防ぐとは、次の3点を仕組みとして用意することです。
- 変更が入った瞬間に、検証が自動で走る
- 結果が遅延なくフィードバックされる
- 失敗時に、誰がどう直すかが明確
CIはこの「構造」を担います。個人の集中力ではなく、変更が起きた事実そのものに反応して検証が走る場を作るものです。
1-2. フィードバックループは1つではない
CIで走らせたい確認は、1種類だけではありません。コードがエンジニアの手元を離れて本番に届くまでには、性質の違う複数のループが並んでいます。
ここで「コミット前」「コミット後」とは、コードをチームの共有リポジトリに送る前と後のことです。
コミット前は自分の手元での確認、コミット後はチームのリポジトリに入った後の検証を指します。
各ループには性質の違いがあります。
| ループの種類 | 速さ | 代表的に捕まえる欠陥 |
|---|---|---|
| ローカル編集ループ | 秒〜分 | コンパイルエラー、自分が今いじっている関数のロジック誤り |
| コミット前ループ | 分単位 | 単体の振る舞いの崩れ、限定範囲のつなぎの誤り |
| コミット後ループ | 十数分〜時間 | 他コンポーネントとの統合不整合、広いユースケースの破綻 |
| RCループ | 時間オーダー | 設定込みの妥当性、本番に近い環境での問題 |
| 本番監視ループ | 常時 | 実トラフィックでしか出ない不具合、外部依存の障害 |
ループは右に行くほど現実に近く、見つけられる欠陥が深くなります。代わりに、フィードバックは遅くなります。
1-3. 「速さ」と「信頼性」を同時に上げる方法
検証範囲を広げると遅くなり、速さを優先すると見落としが増える。
1本のループの中で、速さと検証範囲をどちらも最大化するのは難しいです。
そこで、1本のループに全部を詰め込まず、複数のループに分けて考えます。
それぞれのループに役割を分ければ、手元では早く気づき、後段では広く確認できます。
この記事では、この「ループごとの分業」を中心に考えます。
2. ループの配置: コミット前/コミット後/RC
ループが複数あると分かったところで、次の問いは「何をどこに置くか」です。どこに置くかは、そのループにどれくらいの速さと検証範囲を求めるかで決まります。
2-1. コミット前ループの役割: 速さと信頼性の両立
コミット前ループは、エンジニアが「コミットして次の作業に進む」までの待ち時間に直撃します。
ここを遅くしたり不安定にしたりすると、ループそのものが無視されはじめます。
「またフレーキーで失敗したから再実行しよう」「赤いけど自分のせいじゃないから進める」が常態化したら、もう機能していません。
コミット前に置くべきものは、次の条件をすべて満たすものです。
- 数分以内に終わる
- 外部依存がない、または十分に隔離されている
- 同じコードに対して何度実行しても同じ結果になる
具体的には、単体テストや、限定された範囲の統合テストが該当します。
ここに置いてはいけないのは、遅いテスト・外部のステージング環境を呼ぶテスト・タイミングに依存して不安定になるテストです。
「ここを通った変更の大半は後段でも通る」前提を逆手に取って、通らない少数はコミット後で受け止める設計にします。
2-2. コミット後ループの役割: 速さを少し緩めて広く検証する
コミット後ループは、共有リポジトリに入った後に走る検証です。コミット前より少し時間をかけてよい代わりに、検証の範囲を広げます。
ここに置くべきものは次のとおりです。
- 統合テスト
- ユーザー視点の主要な操作を確認する受け入れテスト
- 実行に時間のかかるテスト
失敗を検知したときに、コミット作者がまだその変更の文脈を覚えている時間内に通知が届くことが重要です。
翌日になると、自分が何をしたか思い出すコストが急に跳ね上がります。
コミット後で失敗が見つかった場合の運用は「直すか、戻すか」の2択です。
直すのが速いなら直す。時間がかかりそうなら、いったんロールバックする。
この判断を素早く回す仕組みは、第4章で改めて扱います。
2-3. RCに対する再検証
ここで RC という言葉が出てきました。
RCは、本番に出す一歩手前のビルド一式です。コードだけでなく、設定や依存ライブラリも固めた状態で、「これを本番に出す候補」として扱います。
RC段階を設ける場合は、コミット後ループで通った検証のうち、本番用の設定や成果物に関わるものを再確認します。「同じテストをなぜまた」と疑問に思うかもしれません。理由は4つあります。
- 健全性チェック: RC用に再ビルドした結果、奇妙なことが起きていないか確認する
- 設定の同時テスト: コードだけでなく、本番用の設定込みで動くかを見る
- 緊急変更の窓口: 急ぎの修正を入れる場合、全テスト完走を待たずに最小限の検証で出すルートとして機能する
- 監査性: 後から「このリリースは何のテストを通ったか」を辿れるようにする
設定をテストするという観点は見落とされがちですが、本番で起きる問題には、「コードそのもの」ではなく設定値や環境差分が原因になるものも少なくありません。
設定をRCの一部としてコードと一緒にテストする発想は、ここに効きます。
2-4. ループを跨いだ役割分担
3つのループの役割を整理すると、次のような分業になります。
| ループ | 速さ | 信頼性要求 | 範囲 | 失敗時の運用 |
|---|---|---|---|---|
| コミット前 | 高速(分単位) | 高い(不安定NG) | 小さい・局所的 | その場で直してから進む |
| コミット後 | 中速(十数分〜時間) | やや高い | 広い・統合範囲 | 直すかロールバック |
| RC | 中速〜(時間オーダー) | 高い(設定込み) | 環境込みの妥当性 | 通らなければリリース見送り |
ここで自分が腑に落ちたのは、「1つのテストをどこに置くか」が品質と速度を同時に決めるという感覚でした。
同じテストでも、置く場所を間違えるとループ全体が機能しなくなります。
例えば、ToDoアプリで「ToDo追加機能」を実装したとして、配置の感覚は次のようになります。
- 関数の入出力をチェックする単体テスト → コミット前
- APIを叩いてDBに保存されることを見るテスト → コミット後
- 本番に近い設定でブラウザ操作まで通すE2Eテスト → RC段階
3. ループの信頼性を支える: 密閉化と記録/再生
配置を決めても、各ループ内のテストが不安定だと全体は機能しません。ここからはループの中身、特に「テストの安定性」をどう確保するかを扱います。
3-1. フレーキーテストはなぜ恐ろしいのか
フレーキーテストとは、失敗するときと通るときがあるテストのことです。同じコードに対して、ある日は緑、次の日は赤、また次の日は緑、という挙動をします。
フレーキーが恐ろしいのは、単に直すのが面倒だからではありません。
- 結果が信用できなくなる。緑が出ても「たまたま通っただけかも」、赤が出ても「いつものフレーキーだろう」と疑い始める
- 周囲のエンジニアの「自分の変更には関係ない」という確信が侵食される
- 量が増えると「テスト結果を見ない」運用に陥り、CI全体が事実上停止する
個別の原因(タイミング依存、乱数、共有資源の競合など)で単発的に起きることもあります。
ただし量が増えてきたら、それは「個別のテストの書き方が悪い」というより、「ループに外乱が漏れ込んでいる構造の問題」と読むほうが筋がいいです。
外部依存・共有環境・本番との通信などが、テストの結果を揺らしているサインだからです。
3-2. 密閉化: 外部依存をループの外に出す
密閉テストとは、外部依存を遮断したテストのことです。テスト対象のアプリケーションと、それが必要とするリソースを、テスト用に閉じた環境内で完結させます。
密閉化の利点は3つあります。
- 同じ入力なら、同じ結果が返ってくる
- 本番側の障害に引きずられない
- 誰がどこで実行しても、同じ条件で確認できる
一方で、完全に密閉するとテストでは緑だったのに本番では落ちる事故が起きやすくなります。
つまり、密閉しすぎると「本番にどれだけ近い確認になっているか」が弱くなります。
そのため、すべてを同じ密閉度にするのではなく、ループごとに変えます。
コミット前は完全密閉、コミット後はAPIの一部を本物に、RCは本番に近い環境、本番監視は本物と、段階を分けることで、テストの安定性と本番に近い確認を両立しやすくなります。
3-3. 記録/再生: 速さと忠実性の中間策
完全密閉と本番通信の中間に位置する手法が、記録/再生です。一度本番に近い環境でレスポンスを記録しておき、テスト時にはその記録を再生して使います。
| 手法 | 決定性 | 速度 | 本番への近さ | 向く配置 |
|---|---|---|---|---|
| フェイクで完全に閉じる | 高い | 速い | 低い | コミット前 |
| 記録/再生 | 高い | 速い | 中程度 | コミット前〜コミット後 |
| 本物のバックエンドに接続する | 低い | 遅い | 高い | RC〜本番監視 |
ただし、記録/再生にも注意点があります。
記録が古いと、今の本番とは違う挙動を正しいものとして扱ってしまいます。
逆に、記録が足りないと、本物なら問題なく動くケースまで失敗扱いになることがあります。
そのため、記録/再生を使うなら、記録をいつ更新するかまで決めておく必要があります。
それでも、ループに外乱を入れにくくしつつ、本番に近い挙動もある程度取り込める手段として有効です。
3-4. 「テスト失敗は本当の問題のときだけ」という設計原則
ループを長く機能させるには、テストが鳴ったときに「本当に何かが壊れている」と信じられる状態を保つ必要があります。
テスト1本ごとに、次の2点を満たしているかを意識します。
| 観点 | 満たされている状態 |
|---|---|
| 鳴る条件 | 守るべき振る舞いが壊れたときだけ赤を出す(任意の閾値や偶然の挙動に依存しない) |
| 鳴った後 | 誰がどう調査して何を直せばいいかが分かる(失敗が行動可能) |
この2つが揃って初めて、ループは持続します。
4. 赤くなった後の動きを決めておく
ループが信頼できるとして、次の関心は「失敗を見つけた後どう動くか」です。検知だけでは不十分で、検知後の運用設計がループ全体の信頼性を決めます。
4-1. 失敗を発見するスピードと修正コストの関係
同じバグでも、発見されるタイミングによって修正コストは大きく変わります。
理由はシンプルです。
- コミット作者がまだ手元の文脈を覚えているうちなら、直すのは早い
- 翌日になると「自分は何をしたっけ」を思い出すコストが乗る
- 本番まで漏れると、影響範囲の調査・ユーザー対応・データ修復まで芋づる式に増える
各ループの「速さ」は、平均修正コストに大きく影響します。だから速いループを左に置く設計が効くのです。
4-2. 直すか戻すかを早く判断する
失敗を見つけたとき、つい「直そう」と考えますが、実は「戻す」ほうが速くて安全なことが多いです。
ロールバックとは、変更を取り消して前の状態(既知の良い状態)に戻すことです。
前進と後退がペアで設計されていると、テストの信頼性は支えられます。
「テストが赤になったら戻す」が即座にできる前提があるからこそ、「赤を信じて止める」という判断ができます。
判断の目安は次の3つに分けられます。
| 状況 | 対応 |
|---|---|
| 原因が明らかで小さく直せる | その場で修正して進める |
| 原因調査に時間がかかる | いったんロールバックする |
| テスト自体が怪しい | 隔離して原因を調査する |
失敗パターンが十分に明確なら、自動ロールバックを選択肢に入れられる場合もあります。「これは戻して間違いない」の判断が形式化できれば、人間を介さずに既知の良い状態へ戻す仕組みも可能です。
なお、ロールバック運用が機能する前提として「失敗のオーナーシップ」があります。
自分の変更が原因なら、その失敗が起きた領域が自分の専門外でも、責任を持って直す姿勢です。
「テストの担当じゃないので」「フロントは分からないので」と責任を投げ合うと、ループは機能しなくなります。
4-3. 信頼できるグリーンを守る
すべてのテストを常に完璧にグリーンにしようとすると、原因調査や一時的な環境問題への対応にコストがかかりすぎることがあります。
大事なのは、既知のフレーキーや一時的な環境問題を本線の判定に混ぜ続けず、「この緑は信じてよい」と言える範囲を守ることです。
| 状況 | 対応 |
|---|---|
| 本当の不具合で赤 | 直す、または既知の良い状態にロールバック |
| 原因が分かっている既知のフレーキー | タグ付けして分離し、残りのテストの信頼性を守る |
| 環境問題で赤 | 再実行で通るなら一時許容、頻発するなら基盤側を直す |
既知のフレーキーや既知の壊れ箇所を、本線に混ぜたまま放置すると、「全体の赤が常態化 → 誰も見ない」のスパイラルに入ります。
タグで分離して「その他のテストはグリーンであることに集中する」運用に切り替えると、テストへの信頼が守れます。
おわりに
CIを「ループの集合」として見る視点に切り替えると、改善の打ち手が具体的になります。
「テスト本数を増やす」「マシンを増強する」「並列化する」より先に、「どのループに何を置いているか」を見直す。
配置がきちんとできていれば、本数や並列数の調整は後から効きます。
この見方は、リードタイム・デプロイ頻度・復旧時間・変更失敗率といった指標にもつながります。
単にCIを高速化するだけでなく、どの段階で何を検知し、赤くなった後にどう戻すかを決めておくことが、結果として開発全体の流れに効いてきます。
AIでコードを書く量や速度が上がるほど、変更を受け止める仕組みの重要性も上がります。
フィードバックが遅いと、問題に気づく前に次の変更を重ねてしまう。
だからこそ、変更が増えても早く気づき、必要ならすぐ戻せるフィードバックループを用意しておくことが重要になります。


