はじめに
「テストはたくさん書いている。カバレッジも80%を超えている。なのにリリースしたらバグが出る」
——そんな経験はありませんか?
自分も最初は、ケースを増やせば品質が上がると思っていました。
けれど、ひたすらケースを足しても欠陥が漏れ続ける。
原因はシンプルで、テストには「捕まえられる欠陥の種類」が違う複数の手法があり、
思いつきで書くと特定の欠陥だけを何度も叩くことになるためです。
この記事ではその違和感を入口に、
テストを「異なる欠陥を狙う多層防御」として再整理します。
ドメイン固有の課題(モバイル・Web・セキュリティ等)はスコープから外し、
ドメイン非依存の基本手法と使い分けに集中します。
TL;DR
- テストは均質な活動ではなく、段階(コンポーネント/統合/妥当性確認)と視点(ホワイトボックス/ブラックボックス)の組み合わせで異なる種類の欠陥を狙う多層防御
- 内部構造を狙う視点と外部仕様を狙う視点は補完関係。
両方を組み合わせて初めてコンポーネント単独の欠陥取りこぼしが減る - 結合段階・要件レベルでも別の種類の欠陥が現れるため、
それぞれを別の段階で狙う設計が必要
テスト戦略の全体像 ── 目的と「完了」の基準
技法を並べる前に「テストは何のための活動か」「いつ終わるのか」を揃えておきます。
土台が曖昧なままだと「とりあえず動くテストを書く」状態から抜け出せない気がしています。
「テスト=バグ探し」という捉え方の落とし穴 ── V&V
ソフトウェアテストは、より広い概念 Verification & Validation(V&V) の一部です。
| 観点 | 検証(Verification) | 妥当性確認(Validation) |
|---|---|---|
| 問い | 正しく作っているか? | 正しいものを作っているか? |
| 対象 | 仕様 vs 実装 | 顧客の期待 vs 実装 |
| 代表手法 | ホワイトボックス・ブラックボックス・統合 | 受け入れテスト・α/βテスト |
| 例 | 金額計算が仕様どおり動くか | ユーザーがこの機能で課題を解決できるか |
検証の網をいくら細かくしても、
作っているモノが顧客の欲しいモノでなければ妥当性確認の側で欠陥として現れます。
V&Vは技術レビューやユーザビリティテストなど多様な活動を含む広い概念で、
テストはその一部です。
「品質はテストで作り込めない」という古典的な言い回しがあります。
テスト開始時点で品質が無ければ終了時点にも無い、という話です。
品質を作るのは設計とレビューと開発プロセス全体で、
テストは確認の最終手段に過ぎません。
テストレベルのスパイラル ── どこから始めてどこまで行くか
テストは「小さな単位から始めて、徐々に組み合わせを広げる」漸進的な戦略を取るのが基本です。
外側に進むほどスコープが広がる入れ子構造で、各層で狙う欠陥が変わります。
| 層 | スコープ | 狙う欠陥 |
|---|---|---|
| コンポーネント(ユニット) | 個別のクラス・関数 | 内部ロジック・境界・状態の誤り |
| 統合 | コンポーネント間 | インターフェース・データの受け渡し |
| 妥当性確認 | アプリ全体 | 顧客の期待との乖離 |
| システム | ハード・人・DB含む全体 | 環境・運用との結合の問題 |
この記事ではコンポーネントから妥当性確認までを扱います。
「全部作ってから最後に一気にテストする」アプローチ(後述のビッグバン統合)は、
現実にはほぼ失敗します。
「テストはいつ終わるのか」という古典的な問い
古い言い回しに「テストは決して終わらない。
開発者からエンドユーザーに負担が移るだけ」というものがあります。
実務では、以下のような指標を組み合わせて「いったんの終わり」を判断します。
- カバレッジ閾値(例: 行カバレッジ80%)
- 既知欠陥カテゴリの充足率(例: 境界値ケースが要件ごとに揃っている)
- 受け入れ基準(顧客代表が合意した基準を満たしている)
カバレッジは「テストが届いたコード」を示しますが、
「テストが正しく検証している」ことは保証しません。
「呼んだだけでアサーションのないテスト」でも行カバレッジは上がるため、
数字だけ追うと欠陥は漏れ続けます。
(特にAIに欠かせたテストはたまにモックだらけで何もしてないことがあります...)
通底する原則として、良いテストケースは
「欠陥検出力が高く・冗長でなく・単純すぎず複雑すぎない」の3点で評価でき、
以降、各技法の共通する軸になります。
テストケース設計の出発点 ── 要件・ユースケースから何を導出するか
全体像を押さえたら、次の問いは「具体的なテストケースをどこから導くか」です。
実務では思いつきやHappy Pathだけで書きがちですが、それでは欠陥を取りこぼします。
出発点として、以下の3つから体系的にケースを引き出す発想に切り替えます。
- 要件・ユースケース(正常系・代替シナリオ・例外シナリオ)
- アンチ要件(してはいけないこと)
- トレーサビリティ(どのケースが何を検証しているか)
要件とユースケースを起点にする
機能要件はユースケースから直接導出でき、シナリオ別にテストケースの種になります。
| シナリオ | 例 | 種となるテスト |
|---|---|---|
| 主シナリオ(正常系) | ユーザーがログインに成功する | Positive |
| 代替シナリオ(条件分岐) | 2要素認証が有効な場合 | 分岐網羅 |
| 例外シナリオ(エラー) | パスワード入力ミス | Negative |
一方、非機能要件(ユーザビリティ・信頼性等)は分析モデルだけでは導出が難しく、受け入れ基準から書き起こす必要があります。
ポジティブとネガティブ ── 「してはいけないこと」のテスト
テストの主目的を「以前未知だった欠陥を発見すること」と捉え直すと、景色が変わります。「正しく動くこと」を示すだけのテストは、目的の半分しか果たしていません。
| カテゴリ | 目的 | 例 |
|---|---|---|
| Positive(正常系) | 仕様どおり動くことを示す | 正しいパスワードでログインできる |
| Negative(異常系) | 不正な入力でも安全に振る舞うことを示す | 間違ったパスワードでログインできない |
| Anti-requirement(アンチ要件) | してはいけないことをしないことを示す | SQLインジェクションでログインできない |
アンチ要件は脅威分析や悪用ケース(abuse case)と接続する考え方です。
自分もログイン機能を書くときはつい正常系ばかりに目が行きがちでした。
本当に欠陥を狙うなら、
以下のようなアンチ要件側のケースを意識的に追加する必要があります。
- 「正解に近い値ではログインできない」
- 「SQLインジェクションペイロードでログインできない」
トレーサビリティ ── テストが何を検証しているかを追える状態
各テストケースは特定の要件(機能・非機能・アンチ要件)にトレース可能であるべき、
という原則があります。
特に回帰テストで、再テスト対象を選ぶ際にトレーサビリティがないと範囲を絞れません。
| テストID | 対応要件ID | 前提条件 | 入力 | 期待結果 |
|---|---|---|---|---|
| TC-001 | REQ-LOGIN-01 | 未ログイン | 正しいID/PW | ホーム画面遷移 |
「対応要件ID」のカラムが要で、これがあるかないかでトレーサビリティの有無が決まります。
内部構造の欠陥を狙う ── ホワイトボックステスト
テストケースの出発点が分かったところで、
次は「コンポーネント内部のロジックに潜む欠陥」を捕まえる手法に踏み込みます。
ホワイトボックステストは内部の制御構造を見ながら設計する手法で、
外部仕様だけでは見逃しがちな欠陥を狙います。
ホワイトボックステストとは何か
ホワイトボックステストは、
コンポーネントの制御構造からテストケースを導出する設計手法です。
Structural Testing、Glass-box Testingとも呼ばれます。
主な保証は4つあります。
- すべての独立パスが少なくとも1回実行される
- すべての論理判断が真と偽の両側で評価される
- すべてのループが境界条件と動作範囲内で実行される
- 内部データ構造の妥当性が確認される
ここで強調したいのは、ホワイトボックスとブラックボックス(後述)の関係です。
両者は「内部構造を見る視点」と「外部仕様だけを見る視点」の対比で、
状況に応じて並行的に使うものだと捉えています。
基本パステストとサイクロマティック複雑度
基本パステスト(Basis Path Testing)は、ホワイトボックスの代表的な技法の1つです。
プログラムを「フローグラフ」(ノード=処理、エッジ=制御の流れ)として表現し、独立したパスの集合(基本セット)を導出します。
基本セットを実行すれば「全ての文が少なくとも1回実行される」ことが保証されます。
独立パスの数はサイクロマティック複雑度 V(G) と呼ばれ、
述語ノード(条件分岐)の数 P から V(G) = P + 1 で求まります。
V(G)は「テストすべき独立パスの上限値」と「コードの論理的複雑さの定量指標」の両面で使えます。
経験則として「V(G)が10前後を超えると複雑さが急に増す」という目安が知られていて、ツールが警告を出す閾値として活用するのが実務的です。
V(G)が低いから欠陥がない、というわけではありません。「複雑だからバグが出やすい」のは確率の話で、複雑度の低い関数にも当然バグはあります。
ループテスト ── 最も欠陥が集中する場所
基本パステストは強力ですが、ループ周りはそれだけでは不十分です。
最も誤りが起きやすいのはループの境界、という経験則があります。
ループテストは単純ループに対して5観点でケースを設計します(nは許容される最大繰り返し回数)。
| 観点 | 内容 | 狙う欠陥 |
|---|---|---|
| ① スキップ | 0回も通らない | 初期状態の処理漏れ、空入力時のクラッシュ |
| ② 1回 | ちょうど1回通る | 1回目固有の処理漏れ、初回フラグ |
| ③ 2回 | 2回通る | 状態継承、累積処理の誤り |
| ④ m回(m<n) | 中間値で通る | 一般的な処理経路の正常系 |
| ⑤ n−1, n, n+1回 | 上限境界 | オフバイワン、上限超過時の例外 |
ネストしたループの場合は、組み合わせ爆発を抑えるため、
内側から順に他のループを最小値に固定しながらテストする戦略が知られています。
地味に重要なのは、「境界に欠陥が集中する」という法則が次のブラックボックスでも繰り返し現れることです。
ホワイトボックステストの限界
ホワイトボックスは内部構造を見る視点なので強力ですが、
以下の欠陥は捕まえにくいです。
- 仕様自体が間違っている(書かれているとおりに動いても要件と違えば検出できない)
- 必要な機能が抜け落ちている(コードがそもそも存在しないものはフローグラフに現れない)
- インターフェースの誤った使用(呼び出し元の使い方の誤り)
これらはブラックボックス・統合テスト・妥当性確認の側で捕まえる分担になります。
「多層防御」が必要な理由が見えてきます。
外部仕様の欠陥を狙う ── ブラックボックステスト
コードの中身を見たら、次は対極のアプローチに移ります。
内部を一切見ずに仕様だけからテストを設計する手法で、
ホワイトボックスの代替ではなく補完です。
ブラックボックステストとは何か
ブラックボックステスト(機能テスト、振る舞いテストとも呼ぶ)は、
機能要件に焦点を合わせ内部の制御構造を意図的に無視するテスト設計です。
狙う欠陥は5カテゴリに整理されています。
| カテゴリ | 典型例 |
|---|---|
| ① 不正/欠落した機能 | 仕様書の機能が実装されていない、別物になっている |
| ② インターフェース誤り | 引数順序・型の誤り、API契約と実装の乖離 |
| ③ データ構造/DBアクセスの誤り | クエリ結果型の不一致、テーブル定義との不整合 |
| ④ 振る舞い/性能の誤り | 結果は返るが処理時間が要件超過、メモリリーク |
| ⑤ 初期化と終了の誤り | 初期化漏れ、リソースのクローズ漏れ |
これらの多くはコードの内側を見ても気付きにくく、
外部仕様から見て初めて違和感が浮き上がります。
だからこそホワイトボックスと組み合わせる意味があります。
同値分割と境界値分析
同値分割は、入力ドメインを「同じ振る舞いをするはずのグループ(等価クラス)」に分割し、各グループから代表値を選ぶ手法です。
「Aで欠陥が出ないなら同じ等価クラスのBでも欠陥は出にくい。
だから代表値1つで十分」という発想です。
境界値分析(BVA)は「欠陥は入力ドメインの中央より境界に集中する」という経験則に基づく技法で、等価クラスの「端」の値を狙います。
| 手法 | 焦点 | 代表値の選び方 | 例(年齢18-65) |
|---|---|---|---|
| 同値分割 | グループの内側 | 各クラスから任意の1つ | 30, 10, 80 |
| 境界値分析 | グループの境界 | 境界とその直前・直後 | 17, 18, 19, 64, 65, 66 |
これをPythonの簡単な関数で書くと、テストケースは以下のようになります。
def is_valid_age(age: int) -> bool:
return 18 <= age <= 65
# 同値分割(各クラスから1つ)
assert is_valid_age(30) is True # 有効クラス
assert is_valid_age(10) is False # 18未満
assert is_valid_age(80) is False # 65超
# 境界値分析(境界の直前・境界・直後)
assert is_valid_age(17) is False
assert is_valid_age(18) is True
assert is_valid_age(19) is True
assert is_valid_age(64) is True
assert is_valid_age(65) is True
assert is_valid_age(66) is False
無意識にBVAをやっていることは多いですが、意識的・体系的にやると漏れが減ります。
ブラックボックステストの限界
ブラックボックスも仕様駆動なので、以下の欠陥は捕まえにくいです。
- パフォーマンスの内部的な劣化(仕様通りの結果は返るが、リソースを浪費している)
- 状態に依存する複雑な振る舞い(OOソフトウェアでは特に顕著)
- デッドコード(到達不可能な分岐)
両方を組み合わせて初めて、コンポーネント単独レベルの欠陥カバレッジが上がります。
状態とシーケンスの欠陥を狙う ── オブジェクト指向ソフトウェアのテスト
現代の多くのソフトウェアはオブジェクト指向(OO)で書かれています。
オブジェクト指向独自の振る舞い軸では、
ホワイトボックス・ブラックボックスだけでは捕まえにくい欠陥が現れます。
- 状態(インスタンスの内部状態によって同じ操作の結果が変わる)
- シーケンス(メソッド呼び出しの順序によって振る舞いが変わる)
ここでの主な手法はクラステストと振る舞いテストの2つで、
コンポーネントテストのオブジェクト指向への拡張として理解するとスッキリします。
オブジェクト指向で何が変わるか ── テスト単位の再定義
オブジェクト指向ではカプセル化により、クラス(属性 + 操作)が一体となります。
テストの単位は個別の関数(メソッド)ではなくクラス全体になります。
| オブジェクト指向の特性 | テストへの影響 | 生まれる欠陥タイプ |
|---|---|---|
| カプセル化 | メソッドが内部状態に依存する | 状態組み合わせ依存の欠陥 |
| 継承 | 親メソッドが子で微妙に異なる振る舞いをする | 意図しないオーバーライド、前提違反 |
| 多態性 | 実行時の実装ディスパッチが状態に依存する | ディスパッチの誤り |
| 状態 | 同じメソッドでも結果が状態で変わる | 操作シーケンス依存の欠陥 |
「状態を考慮せずにメソッドだけテストしても、状態組み合わせの欠陥は見えない」
── これがオブジェクト指向テストの出発点です。
クラステスト ── 操作のシーケンスを設計する
クラステストは、主に「操作シーケンス依存」の欠陥を狙う、
シーケンス自体をテストの主役に据える手法です。
SaaSのサブスクリプションクラスを例にイメージしてみます。
Subscription → start_trial → activate → [ renew | upgrade | downgrade | suspend ]ⁿ → cancel
この「最小行動寿命」(クラスインスタンスの生まれてから死ぬまでの基本シーケンス)を出発点に、いくつかバリエーションを設計します。
# r1: トライアル開始 → 有効化 → 更新を2回 → キャンセル
sub = Subscription()
sub.start_trial(); sub.activate()
sub.renew(); sub.renew()
sub.cancel()
# r2: アップグレードと一時停止/再有効化を含むシーケンス
sub = Subscription()
sub.start_trial(); sub.activate()
sub.upgrade(); sub.suspend(); sub.activate()
sub.cancel()
可能なシーケンスをすべて試すと組み合わせ爆発を起こすため、ここでも同値分割の発想を応用し、「同じ振る舞いをするはずのシーケンス」を1つの代表で済ませます。
振る舞いテスト ── 状態遷移図を駆動する
振る舞いテスト(状態ベーステスト)は「状態依存」の欠陥を狙います。
同じ操作でも現在の状態によって結果が変わるため、
状態遷移を網羅する設計が必要になります。
状態遷移図からテストを導出するコツは、幅優先でモデルを辿ること。
1つのテストケースで1つの遷移を実行し、新しい遷移をテストするときは既にテスト済みの遷移のみを使う方針です。
例として、最小シーケンスから順にテストケースを増やしていく形になります。
- s1:
start_trial → activate → cancel(最短経路) - s2:
start_trial → activate → renew → cancel(active のループを1回) - s3:
start_trial → activate → suspend → activate → cancel(suspended への往復)
「大半の振る舞いは active 状態で起きる」という構造に注目すると、
ここを集中的にテストでなぞる設計が自然に出てきます。
継承階層・多態性ディスパッチの欠陥タイプもありますが、
オブジェクト指向設計の話題に近づくため本記事では深入りしません。
結合の瞬間に現れる欠陥を狙う ── 統合テスト
コンポーネント単独のテストが終わったら、
次は「組み合わせた瞬間に現れる欠陥」を捕まえる段階に入ります。
- 全部単独で動いていたのに、つなげたら動かない
- データがインターフェースで失われる
- あるコンポーネントが別のコンポーネントに予期しない副作用を与える
これらは統合の瞬間にだけ現れる欠陥で、
コンポーネントテストでは構造的に見つけられません。
なぜ「ビッグバン統合」は失敗が約束されているか
素朴な発想として「全コンポーネントを単独テストしてから一気に組み立てて全体テストすれば良いのでは?」が浮かびます。
これがビッグバン統合で、実際にはほぼ失敗します。
- インターフェースで欠陥が起きると、原因の特定が困難(全コンポーネントが容疑者)
- 1つの欠陥が他に連鎖し、根本原因が埋もれる
- 修正してもまた別の欠陥が出るループに入る
答えは漸進的統合(Incremental Integration) です。
小さな増分で組み立て、各段階でテストする戦略を取ります。
トップダウン統合 vs ボトムアップ統合
漸進的統合の伝統的な2大戦略が、トップダウン統合とボトムアップ統合です。
モジュールツリー(M1がmain、下に行くほど下位モジュール)
┌──────┐
│ M1 │
└──┬───┘
┌────────┼────────┐
▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐
│ M2 │ │ M3 │ │ M4 │
└─┬──┘ └────┘ └─┬──┘
▼ ▼
┌────┐ ┌────┐
│ M5 │ │ M6 │
└────┘ └────┘
【トップダウン】M1 から下に向けて統合
└ 未実装の下位モジュールはスタブ(ダミー)で代替
【ボトムアップ】M5/M6 から上に向けて統合
└ 上位モジュールの代わりにドライバ(駆動コード)を書く
それぞれの特徴を表で比較します。
| 観点 | トップダウン | ボトムアップ | ビッグバン | CI |
|---|---|---|---|---|
| ドライバ | 不要 | 必要 | 不要 | 必要に応じて |
| スタブ | 必要 | 不要 | 不要 | 必要に応じて |
| 早期検出できる欠陥 | 主要な制御・意思決定 | 低レベル機能・データ構造 | (特定困難) | 統合時の不整合 |
| 適用場面 | 制御ロジックが複雑 | ユーティリティ層が厚い | 推奨されない | アジャイル/DevOps 標準 |
現代では純粋なトップダウン/ボトムアップを律儀に守るプロジェクトは少なく、
CI環境で「変更があったところを継続的に統合する」ハイブリッド戦略が一般的です。
スタブとドライバ自体も、
テストダブル(モック・フェイク・スパイ・スタブ)として体系化されています。
継続的統合(CI)とスモークテスト
継続的統合(CI)は、
日次以上の頻度で新しいコンポーネントを取り込みながらビルド・テストする実践です。
その中で使われるスモークテストは、完全網羅を目指さず、エンドツーエンドで主要機能を一通り実行して「致命的に壊れていないか」を高速に確認します。
CI/CDパイプラインの全体像をフローチャートで見ると、以下のような分岐構造になります。
スモークテストには分かりやすいメリットがあります。
- 統合リスクの最小化(互換性問題の早期発見)
- 欠陥診断の単純化(毎日テストするので、欠陥が出れば新規変更が原因の可能性が高い)
- 進捗の可視化(毎日「動くソフトウェア」が見える)
回帰テスト ── 変更が既存の振る舞いを壊していないか
新しいモジュールが統合されるたびにデータフローや制御ロジックが変わり、
以前問題なく動いていた機能に副作用を及ぼす可能性があります。
これを捕まえるのが回帰テストで、
既に実行されたテストの一部を変更後に再実行して副作用を確認します。
回帰テストスイートは3層構造で考えると整理しやすいです。
| 層 | 内容 | 役割 |
|---|---|---|
| ① 代表サンプル | 主要機能を実行する代表的なテスト | 全体が壊れていないことの確認 |
| ② 影響範囲フォーカス | 変更で影響を受けやすい機能のテスト | 副作用の早期検出 |
| ③ 変更箇所そのもの | 変更コンポーネントを直接対象とするテスト | 変更内容の正しさの確認 |
ツールとしては自動テストフレームワーク(Playwright、Cypress、pytest等)が現代の主流です。
オブジェクト指向の統合戦略 ── スレッドベースとユースベース
オブジェクト指向ソフトウェアには明示的な階層的制御構造がないため、
トップダウン/ボトムアップという軸はあまり意味をなしません。
代わりにオブジェクト指向特有の2つの統合戦略が使われます。
- スレッドベース統合: 1つの入力やイベントに応答するクラス群を「スレッド」として統合・テスト。ユースケースから自然に導かれる
- ユースベース統合: 他クラスをほぼ使わない独立クラスから始め、独立クラスを使う依存クラスを順に統合する
現代ではこの発想が、マイクロサービス間の契約テスト・E2Eテスト・シナリオテストとして発展しています。
要件と実装の乖離を狙う ── 妥当性確認テスト
統合まで終わったら、最後の関門が妥当性確認テストです。「顧客の期待に応えているか」を確認するテストで、検証(仕様通りか)とは別物です。
発見の遅さ → コンポーネント < 統合 < 妥当性確認 < 本番
修正コスト → 小 中 大 最大
要件レベルの乖離は発見が最も遅れがちで、コストが最も高くつく欠陥カテゴリです。
妥当性確認テストとは何か
妥当性確認テストは、統合テスト後にソフトウェアが「顧客から合理的に期待される方法で機能するか」を確認するテストです。
判断基準はソフトウェア要件仕様(SRS)に書かれた受け入れ基準です。
エンドユーザー視点のブラックボックステストで、以下のような観点を確認します。
- 機能(仕様どおりに動くか)
- 性能(応答時間・スループットなどが期待値を満たすか)
- ドキュメント(操作手順や前提が利用者に伝わるか)
- ユーザビリティ(実際の利用シナリオで使いやすいか)
自分が書いたコードを破壊するテストには心理的抵抗が生じやすいです。
そのため現代では、機能横断的な独立評価チームが開発者と異なる視点でテストを設計・実行する形が広まっています。
具体的にはQA(品質保証)・SRE(運用品質を担う役割)・セキュリティチームなどです。
αテストとβテスト、そして現代的な代替
妥当性確認には、顧客に直接触ってもらうαテストとβテストがあります。
| 観点 | αテスト | βテスト | 受け入れテスト |
|---|---|---|---|
| 実施場所 | 開発者環境(社内) | 実利用環境(社外) | 実利用または契約環境 |
| 参加者 | 社内のターゲットユーザー(顧客代表を含む) | エンドユーザー | 顧客代表 |
| 開発者の立ち会い | あり(観察・記録) | なし(事後報告) | 場合による |
| 現代の実装例 | フィーチャーフラグの社内露出 | カナリアリリース | A/B・段階的ロールアウト |
これらは、αテスト・βテストの「現代的バージョン」と位置付けると整理しやすいです。
- フィーチャーフラグ(機能のON/OFFを動的切替)
- カナリアリリース(一部ユーザーへの先行公開)
- A/Bテスト
構成レビュー(監査)── ソフトウェア配布物の最終確認
妥当性確認プロセスのもう1つの要素が構成レビュー(監査)です。
配布物(コード・ドキュメント・テスト・ビルド成果物)が揃っているかを配布前にチェックする活動です。
現代では、以下のような項目がCI/CDパイプライン内で自動化されることが多いです。
- 依存ライセンスチェック
- SBOM(依存コンポーネントの一覧)生成
- 署名検証
- 脆弱性スキャン
おわりに ── 多層防御としてのテスト
冒頭の問い「テストをたくさん書いているのにバグが減らない」への答えは、
ここまで来て解像度が上がったと思います。
テストは均質な活動ではなく、
段階 × 視点の組み合わせで異なる欠陥を狙う多層防御である、
というのが整理した答えです。
各レベル × 各手法は補完関係にあります。
| 視点 / 段階 | 内部構造(ホワイトボックス) | 外部仕様(ブラックボックス) |
|---|---|---|
| コンポーネント | 基本パス・ループ・複雑度 | 同値分割・境界値分析 |
| オブジェクト指向コンポーネント | クラステスト・振る舞いテスト | 外部仕様への同値分割等の応用 |
| 統合 | 経路カバレッジに基づく統合 | 契約・スレッド/ユース・スモーク・回帰 |
| 妥当性確認 | (ほぼ使わない) | 受け入れ・α/β・構成レビュー |
それぞれのセルに固有の欠陥種類が住んでいて、漏れたものは隣のセルに任せる、
というのが多層防御の発想です。
現代的な配分指針として広く使われているのがテストピラミッドで、
下層を厚く・上層を薄く配分するシンプルな指針として実務の出発点になります。
┌──────────┐
│ E2E │ ← 少なく / 高コスト / システム全体
├──────────┤
│ 統合 │ ← 中程度
├──────────┤
│ ユニット │ ← 多く / 低コスト / 速い
└──────────┘
本記事のテストレベルとの対応:
E2E ← 妥当性確認テスト
統合 ← 統合テスト
ユニット ← コンポーネントテスト
ただし、システムの境界・依存関係に応じて配分モデルは変わります。
フロントエンドやマイクロサービスの文脈では「テストトロフィー」「ハニカム」など別のモデルも提唱されており、形を選ぶのが妥当だと思っています。
最後に、明日からの一歩として持って帰りたいことを並べておきます。
- 自分が今書いているテストが「どの欠陥種類を狙っているのか」を1つずつ言語化してみる
- ブラックボックスばかりで書いていれば、内部構造由来の欠陥が漏れていないか確認する
- ホワイトボックスばかりで書いていれば、仕様の誤りが漏れていないか確認する
- CIパイプラインにスモークテストと回帰テストの自動化を組み込む
- テストケースに対応要件IDを明記してトレーサビリティを整える
「テストはいつ終わるか」への自分なりの答えは、
終わりはない、ただ防御層を増やしていくだけ、です。
戦略的に層を組むことで「ユーザーより先に自分たちで見つけられる確率」は確実に上がる。
それがテスト戦略の本質だと思っています。
