大規模開発のテスト — 数百万件のテストと、 それを支える設計
数万人が単一のモノレポに 1 日数万件コミットし、 数百万件のテストが走る規模になると、 テストの問題は質的に変わる。 hermetic性・テストサイズ分類・Bazel系のキャッシュ・test impact analysisという実行基盤の話と、 DI・ヘキサゴナルアーキテクチャ・Humble Objectというテスト容易な設計の話は、 実は同じ問いに答えている。
規模が変えるのは「量」 ではなく「前提」
数十人のチームが数百件のテストを回すのと、 数万人のエンジニアが単一の巨大なコードベース (モノレポ、 monorepo) に 1 日数万件のコミットを行い、 数百万件のテストを実行するのとでは、 テストにまつわる問題の質が変わる。 Google や Meta のような組織は、 ビルドシステム・実行基盤・フレーキー管理を専用化することで、 この規模のテストを回し続けてきた。 そしてこの規模を支えているのは、 実行基盤だけではない —— テスト容易な設計という、 一見別の話題に見える実践が、 根底で同じ問いに答えている。
モノレポという前提 — Google と Meta の巨大テストスイート文化
Google と Meta は、 事業のほぼ全体を単一のモノレポで管理する運用で知られる。 Google の社内ビルドシステム Blaze (2004 年頃から社内使用、 2015 年に Bazel としてオープンソース化) と、 Meta の Buck (2013 年オープンソース化、 2023 年に Rust 製の後継 Buck2 へ移行) は、 いずれもモノレポ全体を単一のビルドグラフとして扱い、 テストターゲットもその中に組み込む思想を共有する。
この規模では「変更のたびにテストを全部実行する」 という発想自体が非現実的になる。 数百万件を毎回再実行すれば CI のキューは詰まり、 フィードバックは数時間単位で遅延する。 モノレポ運用の核心は、 「変更に関係するテストだけを、 確実にキャッシュを効かせながら実行する」 仕組みをビルドシステムに組み込むことにある。
hermetic tests — 密閉性がすべての土台になる
大規模テストスイートの信頼性を支える中心概念が hermetic (密閉的) なテストである。 これは書籍 Software Engineering at Google でも中心的に扱われる原則で、 次のように定義される。
テストは、 そのセットアップ・実行・後始末に必要な情報をすべて自分自身の中に含んでいるべきである。
hermetic なテストは、 外部の共有データベース・実際のネットワーク・実行環境固有の状態に依存しない。 ディスクアクセスが必要なら、 実ディスクではなくインメモリのファイルシステムを使う、 といった形で環境から隔離される。 hermetic 性は前記事 フレーキーテストとの戦い で扱ったフレーキーの主要原因 (外部依存・ネットワーク・状態共有) を構造的に排除し、 かつ後述するキャッシュを安全に効かせる前提条件になる。
テストサイズ分類 — small / medium / large
Google は、 テストを「何をしているコードか」 ではなく 「どう実行され、 何を許されているか」 という観点で分類する体系を用いる。
| 分類 | 実行環境の制約 | 典型的な実行時間 |
|---|---|---|
| small | 単一プロセス・単一スレッド、 ネットワーク・ディスク I/O 禁止 | 数十ミリ秒〜 |
| medium | 単一マシン内、 localhost 以外へのネットワーク呼び出し禁止 | 数秒程度 |
| large | 複数マシンにまたがってよい | 数十秒〜数分 |
サイズは「単体テストか統合テストか」 という軸とは独立しており、 実行時に課される環境的制約だけで決まる。 Google が推奨する配分の目安は small 約 80%・medium 約 15%・large 約 5% とされ、 前記事 テストの形 で扱ったテストピラミッドの精神を、 モノレポの実行制約という軸で再定式化したものと理解できる。
リモートキャッシュと test impact analysis
Bazel・Buck2 に共通する中核機能が リモートキャッシュ である。 ビルド・テストの入力 (ソース・依存バージョン・コンパイラフラグ) をすべてハッシュ化し、 同じ入力ハッシュに既にキャッシュされた結果があれば、 再実行せずそのまま再利用する。 このキャッシュが安全に機能する前提こそ、 前節の hermetic 性である。 Buck2 は Buck (Java 実装) の Rust 後継で、 Bazel と同じく Starlark (Python ライクな設定言語) を採用しており、 Blaze・Bazel・Buck2 は同じ系譜のツール群として語られる。
キャッシュと並ぶもう一つの核心技術が test impact analysis (テスト影響分析) である。 静的な手法はビルドグラフを辿り、 変更ファイルから影響を受けうるテストターゲットを機械的に特定する (Bazel・Buck2 の query 機能)。 動的・統計的な手法は、 過去の実行履歴から「コード変更のパターン」 と「実際に失敗したテスト」 の相関を機械学習で学習する。 Meta が 2018 年に発表した Predictive Test Selection はその代表例で、 実行するテストを絞り込むことでテスト実行のインフラコストを半減させながら、 個別のテスト失敗の 95% 以上、 欠陥を含むコード変更の 99.9% 以上を検出し続けられると報告されている。 同研究はテストのフレーキーさも考慮に入れてモデル化している点でも参照される。
分散実行とビルドグラフ
数百万件規模のテストは単一マシンの逐次実行では到底間に合わない。 Bazel・Buck2 は、 依存関係のないテストターゲットを並列に実行してよいことをビルドグラフから機械的に保証し、 多数のワーカーマシンへの リモート実行 に分散させる。 「変更に影響するテストだけを、 多数のマシンに分散して並列実行する」 という運用は、 キャッシュ・test impact analysis・分散実行が同じビルドグラフという抽象を共有しているからこそ成立する。 この並列化もまた、 hermetic 性 (テスト同士が共有状態を持たないこと) を前提にしている。
規模が増やすフレーキーの絶対数
規模が大きくなるほど、 前記事で扱ったフレーキーテストの絶対数も増える。 Google が 2016 年に公開した調査 (John Micco ら) によれば、 社内テストの約 16% が何らかの水準でフレーキーな挙動を示した一方、 常にフレーキーな (ほぼ毎回結果が揺れる) テストは約 1.5% 程度だったとされる。 これは個々のチームの怠慢ではなく規模に付随する構造的な課題であり、 大規模組織は継続的な再実行によるフレーク検出・フレーク率が閾値を超えたテストの自動フラグ付け・既知のフレーキー失敗を CI の合否判定から除外するロジックといった専用の仕組みを持つ。
規模が大きくなるほど顕在化する課題は他にもある —— hermetic でないテストが紛れ込むとキャッシュの信頼性そのものが崩れる キャッシュの一貫性、 モノレポの成長にともないビルドグラフ自体が肥大化する 依存グラフの肥大化、 「このテストの責任者は誰か」 が曖昧になる 所有権の希薄化、 そしてリモートキャッシュ・リモート実行に最適化された環境と個々の開発者のローカル体験との乖離である。
実行基盤だけでは足りない — 継ぎ目という発想
ここまでの仕組みはすべて、 個々のテストが hermetic であること —— 本物の DB やネットワークに依存せず、 差し替え可能であること —— を土台にしている。 だがコードがそもそも差し替え可能に書かれていなければ、 hermetic 性は絵に描いた餅になる。 ここで実行基盤の話は、 設計の話に接続する。
Michael Feathers は著書『Working Effectively with Legacy Code』 (2004) で、 テストのないレガシーコードにテストを追加する技法を体系化し、 その中核に 継ぎ目 (seam) という概念を置いた。 継ぎ目とは「実際のコードを編集せずに振る舞いを変更できる場所」 であり、 依存オブジェクトを差し替えられるコンストラクタ引数 (オブジェクトの継ぎ目)、 リンク時に差し替え可能な依存 (リンクの継ぎ目)、 マクロによる差し替え (プリプロセッサの継ぎ目) などがある。 継ぎ目のないコード —— すべてが new でベタ書きされ静的メソッドやグローバル状態に直接依存するコード —— には、 テストの入口も hermetic 性を実現する入口も存在しない。
依存性注入とヘキサゴナルアーキテクチャ
継ぎ目を作る最も一般的な手段が 依存性注入 (DI) である。
// DIなし: 依存を内部で生成 → テストに本物のSMTPサーバが要る
class OrderService {
private EmailSender sender = new SmtpEmailSender();
}
// DIあり: 依存を外部注入 → テストではFakeに差し替え可能
class OrderService {
OrderService(EmailSender sender) { this.sender = sender; }
}
ヘキサゴナルアーキテクチャ (ポート & アダプタ) は Alistair Cockburn が 2005 年に提唱した。 コアロジックを中心に置き、 外部とのやり取りは「ポート」 というインターフェース越しに行い、 具体的技術との接続は「アダプタ」 が担う。 Cockburn の動機は「アプリケーションがユーザー・自動テストのいずれからも等しく駆動でき、 実行時のデバイスやDBから切り離して開発・テストできるようにする」 ことだった。 このアーキテクチャは後年、 クリーンアーキテクチャ (Robert C. Martin) やオニオンアーキテクチャ (Jeffrey Palermo) へと変奏されるが、 いずれも「依存の方向をコアへ向ける」 発想を共有する。
Humble Object と 関数型コア・命令型シェル
Humble Object パターンは、 テストしにくい部分の中身を極力空っぽにする考え方である。 原型は Feathers が 2002 年に発表した論文 “The Humble Dialog Box” に遡り、 GUI のロジックを別クラスに追い出し、 ダイアログ自体を「表示するだけの控えめなクラス」 にすることを提案した。 この考え方は Steve Freeman と Nat Pryce の『Growing Object-Oriented Software, Guided by Tests』 (2009、 通称 GOOS 本) で一般化され、 データベースアクセス層や外部 API 呼び出し層など、 あらゆる「テストしにくい境界」 に適用できる汎用パターンとして定着した。 MVP・MVVM の「View を薄く保つ」 設計もその一実装形態である。
関数型コア・命令型シェル は Gary Bernhardt が 2012 年のカンファレンストーク “Boundaries” (SCNA 2012) で広めた設計スタイルで、 純粋関数のみで構成される「コア」 と、 I/O や副作用を担う薄い「シェル」 に分ける。 コアはモックもスタブも不要で高速かつ決定的にテストできる。 Bernhardt はこのスタイルが Cockburn のポート & アダプタから着想を得たことを認めており、 DI・ヘキサゴナル・Humble Object・関数型コアはいずれも「副作用を境界に押し出し、 ロジックを純粋に保つ」 という同一原理の異なる表現である。
グローバル状態がテストを壊す理由と、 摩擦という警告
シングルトンやグローバル変数は、 テスト間の状態共有による実行順序依存、 差し替え不能、 並行実行時の競合、 コンストラクタに現れない暗黙の依存という 4 つの理由でテストを困難にする。 DI やヘキサゴナルアーキテクチャは、 この「暗黙の依存」 を「明示的な依存」 に変換する手段だと言い換えられる。
テストを書く際の摩擦は、 設計上の警告として読める。
| 摩擦 | 示唆される問題 |
|---|---|
| セットアップが異常に長い | 責務過多のクラス |
| モックだらけになる | 抽象化レベルの不一致 |
| 特定の順序でしかテストが通らない | グローバル状態への隠れた依存 |
| 時刻・乱数のせいで結果が変わる | 副作用が境界に押し出されていない |
保守性という もう一つの主戦場 — テスト臭とDAMP
数百万件規模のテストは、 書き方が悪ければ保守コストの高い負債になる。 Gerard Meszaros は 2007 年の著書『xUnit Test Patterns: Refactoring Test Code』 で 68 種のパターンと テスト臭 (test smells) のカタログを提示した。
| テスト臭 | 内容 |
|---|---|
| Fragile Test | 些細な変更 (振る舞いに影響しないリファクタリング等) でテストが壊れる |
| Mystery Guest | テストの成否を左右する重要なデータが、 テスト外部 (共有フィクスチャ・DB の既存レコード等) に隠れている |
| Assertion Roulette | 1 つのテストに大量のアサーションが並び、 失敗時にどれが原因か判別できない |
| General Fixture | 個々のテストが必要としない余計なデータまで含む共通フィクスチャを使い回す |
| Test Run War | 複数テスト (や並列実行時の複数プロセス) が同じ共有リソース (DB・ファイル・ポート) を奪い合う |
プロダクションコードの世界で共有される DRY (重複を排除せよ) に対し、 テストコードの世界では DAMP (Descriptive And Meaningful Phrases) という対抗的な標語が定着している。 セットアップの共通化を進めすぎると、 あるテストを理解するために複数のヘルパーを遡って読む必要が生じ、 それが Mystery Guest を生む。 実務上は「両者は多くの場面で両立する」 という折衷的な理解が一般的で、 無意味な重複は排除しつつ、 テストの意図を伝える構造までは過度に抽象化しない、 というのが実践知である。
モックの過剰使用も保守性を損なう。 テストが呼び出し順序・回数まで検証する 過剰な仕様化 は、 内部実装の無害なリファクタリングでテストを壊す。 「自分がテストしている対象の直接の協調者だけをモックし、 値オブジェクトは実物を使う」 という節度が広く推奨される。 テストデータの生成には、 典型例を用意する Object Mother と、 デフォルト値を持ちつつ必要な属性だけ上書きできる Test Data Builder の 2 パターンがあり、 後者は General Fixture を避けやすいため近年より広く採用される。
規模と設計は、 実は同じ問いに答えている
一見すると、 モノレポの実行基盤 (hermetic 性・キャッシュ・test impact analysis) と、 個々のクラスの設計 (DI・ヘキサゴナル・Humble Object) は別々の話題に見える。 だが hermetic 性は「依存を差し替えられる設計」 があって初めて実現でき、 small テストが 8 割を占められるのは「副作用を境界に押し出した」 設計があるからであり、 数百万件のテストを人間がレビュー・所有し続けられるのは可読性の高い ── DAMP を意識した ── テストがあるからだ。 大規模開発のテストが教えてくれるのは、 「どう実行するか」 という基盤の問題と「どう書くか」 という設計の問題が、 規模が大きくなるほど分離できなくなる、 という一点である。
この記事から次の記事へ
数百万件のテストを支える実行基盤と設計は、 いずれも人間が積み上げてきた工学的な解答だった。 だが 2020 年代、 テストの生成・修復・評価にまで AI が関わり始めている。 最終記事では、 生成 AI がテストをどう変えつつあるか —— そして何を変えていないか —— を見る。