0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テストの目的と「良いテスト」を測る4本柱

0
Posted at

はじめに

テストを書いているのに、開発がなかなか楽にならない。
新しい機能を入れるたびにどこかが壊れ、テストの修正にも時間がかかる。
——そんな経験はありませんか?

「テストが足りないからだ」と思ってさらにテストを増やしてみたものの、状況はあまり変わらない。
実はこの問題、テストの「量」を増やしても解決しません。
プロジェクトの成長を支えるのは、テストの「質」です。

この記事では、以下の2つを整理します。

  • テストの目的とは何か(そしてカバレッジだけでは質を測れない理由)
  • 良いテストを評価するための「4本柱」フレームワーク

4本柱は「このテストは良いのか、悪いのか」を判断するための共通の物差しです。
レビューで「なぜこのテストが良くないのか」を説明できるようになることを目指します。

TL;DR

  • テストの目的は「持続的成長の実現」。量ではなく質が問われる
  • カバレッジは「テストが不足している」ことは示せるが、「テストが十分である」ことは示せない
  • 良いテストの4本柱(退行への保護・リファクタリング耐性・素早いフィードバック・保守性)が質を測る物差しになる。4つの掛け算で価値が決まり、1つでもゼロなら価値はゼロ

テストの目的 — 持続的成長を支える安全網

テストを書く動機は人によってさまざまです。
「設計が良くなるから」「品質を担保したいから」「ドキュメント代わりになるから」——どれも間違いではありません。
しかし、最も根本的な目的を定義しておかないと、「どこまでテストすべきか」の判断軸がブレてしまいます。

まずゴールを揃えるところから始めましょう。

テストがない世界で何が起こるか

テストなしのプロジェクトは、最初は開発が速いです。
まだ複雑な依存関係もなく、変更が影響する範囲も小さいからです。

しかし、コードベースが成長するにつれて様子が変わってきます。

  • 変更のたびに予期しない箇所が壊れる
  • バグを直したら別のバグが生まれる
  • 1つの修正が連鎖的に他の部分を壊していく

この現象はソフトウェアエントロピーと呼ばれます。
エントロピーとは「系の中の無秩序さ」を表す物理学の概念です。
ソフトウェアにおいては「コードベースの複雑さ・散らかり具合が時間とともに増大していく」傾向を指しています。

テストは、このエントロピーの増大に対抗する安全網です。
既存の機能が壊れていないことを継続的に確認することで、新機能の追加やリファクタリングを安心して行えるようになります。

目的は「持続的成長の実現」

テストの目的を一言で定義するなら、「持続的成長(sustainable growth)の実現」です。

ポイントは「持続的」という部分にあります。
短期的に動くコードを書くのは比較的簡単ですが、長期にわたって開発速度を維持するには、テストの力が不可欠です。

「テストを書くとコードの設計が良くなる」とよく言われます。
これは事実ですが、テストの目的そのものではなく、あくまで嬉しい副作用です。
テストしやすいコード ≠ 良い設計、という点には注意が必要です。
テストしにくいコードは設計に問題がある可能性が高い(良い消極的指標)一方、テストしやすいからといってそれだけで良い設計とは限りません(悪い積極的指標)。

テストもコードであり、コードは負債である

ここで1つ、見落とされがちな事実を確認しておきます。

テストコードもプロダクションコードと同じく「負債」です。
コードは書けば書くほど保守対象が増え、潜在的なバグの表面積も広がります。
テストコードも例外ではありません。

  • テストの修正にかかる時間
  • テストの実行にかかる時間
  • 偽の失敗を調査する時間
  • テストを読んで仕様を理解する時間

これらはすべてコストです。
したがって「テストは多ければ多いほどよい」とは言えません。
価値のないテストは負債を増やすだけです。

質の低いテストばかりのプロジェクトは、テストがないプロジェクトと同じ末路を辿ります。
最初こそ開発速度の低下は緩やかですが、やがて停滞期に入る点は変わりません。

カバレッジという物差しの限界

テストの質が大事だとわかりました。
では、質をどうやって測ればよいでしょうか?

真っ先に思いつくのがカバレッジです。
カバレッジが高ければテストは十分——そう考えるのは自然ですが、実はそこに落とし穴があります。

コードカバレッジの仕組みと弱点

コードカバレッジ(行カバレッジ)は、もっとも広く使われているカバレッジ指標です。

コードカバレッジ = テストが実行したコード行数 / 全コード行数

シンプルでわかりやすい指標ですが、1つ問題があります。
コードの書き方を変えるだけで、カバレッジの数値が変わってしまう点です。

たとえば、if 文を三項演算子に書き換えて行数を減らしたとします。
テストの内容は何も変わっていないのに、行数が減った分だけカバレッジの数値は上がります。
つまり、コードカバレッジは「コードの書き方」に依存しており、テストの質とは独立に変動する指標なのです。

分岐カバレッジ — コードカバレッジの弱点を補えるか

分岐カバレッジは、コードの書き方に左右されないカバレッジ指標です。

分岐カバレッジ = テストが通過した分岐数 / 全分岐数

ifswitch の分岐を基準にするため、コードの整形による数値の変動を受けにくくなっています。
コードカバレッジの弱点を補えているように見えますが、それでも以下の2つの問題が残ります。

カバレッジが高くても質が低い2つのケース

ケース1:コードを「実行」しただけで「検証」していない

テストにアサーション(検証)が一切なくても、コードを実行さえすればカバレッジは上がります。
極端な例として、アサーションのないテストでもカバレッジ100%は達成可能です。

# カバレッジ100%だが、何も検証していない
def test_nothing():
    result = calculate_price(100, 0.1)
    # assertがない → 何も確認していない

これはさすがに極端に見えるかもしれませんが、実際に起こりうる話です。
カバレッジ100%を義務化した組織で、try/catch で例外を握りつぶし、アサーションなしのテストが大量に作られた事例があります。
数字を満たすための無意味なテストが量産されてしまったわけです。

ケース2:外部ライブラリの内部パスが見えない

自分のコードの分岐をすべてカバーしていても、呼び出し先のライブラリやフレームワーク内部のコードパスはカバレッジに計上されません。

たとえば、文字列を整数に変換する処理を考えてみます。
自分のコード上は1行の呼び出しだけですが、変換ライブラリの内部には多くの分岐が隠れています。

  • null が渡された場合
  • 空文字が渡された場合
  • 数値でない文字列が渡された場合
  • 桁あふれが発生する場合

カバレッジ指標にはこれらの分岐が一切反映されないため、エッジケースが見逃される可能性があります。

カバレッジは「目標」ではなく「指標」

ここまでの話をまとめると、カバレッジには次のような性質があります。

  • カバレッジが低い → テスト不足の確かなサイン
  • カバレッジが高い → テストが十分である証拠にはならない

つまりカバレッジは「良い消極的指標(negative indicator)だが、悪い積極的指標(positive indicator)」です。

カバレッジの数値目標を必達にすると、数字を満たすための無意味なテストが量産されます。
重要な部分のカバレッジが高いことは望ましいですが、特定の数値を必達目標にすることは逆効果です。
カバレッジは「目標」ではなく、テスト不足を見つけるための「指標」として使うのが正しい付き合い方です。

4本柱 — テストの質を測る共通の物差し

カバレッジでは「テストが十分か」を判断できないことがわかりました。
では何を使って判断すればよいのか?

ここで導入するのが4本柱フレームワークです。
テストの価値を多面的に評価するための、カバレッジに代わる物差しです。

テストの価値は「掛け算」で決まる

4つの柱を個別に見ていく前に、まず全体像を押さえます。

テストの価値 = 退行への保護
             × リファクタリング耐性
             × 素早いフィードバック
             × 保守性

掛け算であることがポイントです。
足し算なら1つの柱がゼロでも他でカバーできますが、掛け算ではどれか1つでもゼロになると、テスト全体の価値がゼロになります。

つまり、大量の中途半端なテストより、少数でも4つの柱がバランスよく揃ったテストのほうがプロジェクトを支えます。
この前提を念頭に置いて、4つの柱を1つずつ見ていきましょう。

第1の柱:退行への保護

退行(リグレッション)とは、一度正しく動いていた機能がコード変更によって壊れることです。
ソフトウェアバグと同義で使われる言葉です。

退行への保護(Protection against regressions)は、テストがこのバグを検出する力を指します。

退行への保護を高める要素は3つあります。

  • テストが実行するコードの量が多い:多くのコードを通るほど、バグに遭遇する可能性が高まる
  • そのコードの複雑度やビジネス上の重要性が高い:ビジネスロジックのバグは、定型的な配線コード(DTOの定義やDIの設定など)のバグより影響が大きい
  • 外部ライブラリやフレームワークも検証範囲に含まれている:自分のコードだけでなく、依存先の動作も確認している

逆に、1行のプロパティ定義のようなトリビアルなコードをテストしても、退行への保護はほとんど高まりません。
ミスが入り込む余地がそもそも小さいからです。

第2の柱:リファクタリング耐性

リファクタリング耐性(Resistance to refactoring)は、実装の変更に対するテストの安定性を表す指標です。
コードの内部実装を変更したとき、機能が壊れていないのにテストが失敗しない度合いを指します。

機能は正常に動いているのにテストだけが失敗する——これが偽陽性(false positive)です。
医療検査での「偽陽性」と同じ考え方で、「病気ではないのに陽性と判定される誤報」に相当します。

偽陽性が多いと何が起こるか、段階的に見てみましょう。

  1. テストが頻繁に「壊れた!」と警告を出す
  2. 調べてみると、機能自体は正常で、テストが実装の変更に反応しているだけ
  3. この繰り返しで、テスト失敗への反応が鈍くなる
  4. 本物のバグによる失敗も「また偽陽性だろう」と見逃してしまう
  5. テストスイート全体への信頼が失われ、リファクタリング自体を避けるようになる

偽陽性の根本原因は、テストが「何を実現するか(振る舞い)」ではなく「どう実現するか(実装の手順)」に結合していることです。

具体例で考えてみます。
あるHTMLレンダラーが、内部的に複数のサブレンダラーを組み合わせてHTMLを生成しているとします。

  • 偽陽性が起きやすいテスト:「サブレンダラーの型と順序」を検証する → 内部構造を変えるたびに壊れる
  • 偽陽性が起きにくいテスト:「最終的なHTML出力」を検証する → 内部をどう変えても、出力が同じなら通る

リファクタリング耐性を高めるには、テストを実装の詳細ではなく、エンドユーザーにとって意味のある結果(観測可能な振る舞い)に対して書くことが重要です。

第3の柱:素早いフィードバック

テストがバグを見つけてくれたとしても、それが1時間後では手遅れかもしれません。
3つ目の柱は、テストの「速さ」に注目します。

  • テストの実行速度が速いほど、頻繁に実行でき、バグを早期に発見できる
  • 遅いテストはフィードバックループを長引かせ、バグが混入した状態で開発を続けてしまうリスクを高める
  • バグの発見が遅れるほど、修正コストは増大する

素早いフィードバック(Fast feedback)は、テストがどれだけ速くバグの存在を教えてくれるかの指標です。

第4の柱:保守性

テストの価値が高くても、読めない・動かせないテストは結局使われなくなります。
最後の柱は、テスト自体の「扱いやすさ」です。

保守性(Maintainability)は2つの要素で構成されます。

  • テストの理解しやすさ:テストコードが短く、読みやすいこと。何をテストしているのかがすぐにわかること
  • テストの実行しやすさ:外部依存(DB、ネットワークなど)が少なく、環境構築の手間が小さいこと

テストコードもプロダクションコードと同じ「第一級市民」として扱うべきです。
可読性を犠牲にしてテストコードを短縮するのは逆効果で、「理解しやすさ」こそが保守性の核心です。

4本柱のトレードオフ — 銀の弾丸はない

4つの柱をすべて知りました。
では、すべてを最大化すればよいのか?
残念ながら、話はそう単純ではありません。

4本柱の間にはトレードオフがあり、「完璧なテスト」は存在しません。
だからこそ、戦略的に柱を配分する考え方が必要になります。

退行への保護とリファクタリング耐性の関係 — テストの正確性

4本柱のうち、退行への保護とリファクタリング耐性には特別な関係があります。
この2つはテストの正確性(accuracy)を構成する表裏の関係です。

  • 退行への保護が高い → バグの見逃し(偽陰性 / false negative)が少ない
  • リファクタリング耐性が高い → 誤報(偽陽性 / false positive)が少ない

偽陰性とは「バグがあるのにテストが通ってしまう見逃し」のことです。
先ほど登場した偽陽性の逆パターンにあたります。

テストの正確性は、シグナルとノイズの比率で捉えることができます。

テストの正確性 = シグナル(バグを見つける力)/ ノイズ(誤報の量)

シグナルを高め、ノイズを減らすことで、テストの情報としての価値が上がります。
どちらか一方だけでは不十分です。
バグを見つける力がどんなに高くても、誤報だらけでは本物のシグナルが埋もれてしまいます。

プロジェクト初期は偽陽性の害は比較的小さいです。
コードがまだ新しく、リファクタリングの必要性もそれほど高くないからです。
しかしコードベースが成長し、リファクタリングの頻度が増すにつれて、偽陽性の害は偽陰性と同等かそれ以上になっていきます。

最初の3つの柱は同時に最大化できない

退行への保護・リファクタリング耐性・素早いフィードバック、この3つは同時に最大化できません。
2つを最大化すると、残りの1つが犠牲になります。

3つの極端なケースで確認してみましょう。

E2Eテスト:速さを犠牲にするケース

E2Eテストはシステム全体をエンドユーザーの視点で検証します。
実行するコード量が多いため退行への保護は高く、内部実装に依存しないためリファクタリング耐性も高いです。
しかし、システム全体を動かす分だけ実行速度が遅く、素早いフィードバックが犠牲になります。

トリビアルテスト:バグ発見力を犠牲にするケース

1行のゲッターやセッターのような、ミスが入り込む余地のないコードに対するテストです。
実行は高速でリファクタリングで壊れることもないですが、そもそもバグを見つける力がありません。
退行への保護が犠牲になっています。

脆いテスト:リファクタリング耐性を犠牲にするケース

実装の詳細に密結合したテストです。
実行は速く、実装の隅々まで検証するのでバグを見つける力はありますが、リファクタリングのたびに壊れます。
リファクタリング耐性が犠牲になっています。

リファクタリング耐性は妥協すべきでない柱

3つの柱が同時に最大化できないなら、どこを妥協すればよいのでしょうか?

ここで重要なのは、リファクタリング耐性の特殊な性質です。
リファクタリング耐性は「ある程度ある」という中間状態が取りにくく、「ある」か「ない」かの二者択一に近い性質を持っています。
テストが実装の詳細に結合している時点で、リファクタリング耐性はほぼゼロになります。
「少しだけ妥協する」ということができないのです。

したがって、リファクタリング耐性は常に最大化を目指すべき柱です。
掛け算の性質上、ゼロになった柱があればテストの価値は全体としてゼロになるため、「ない」側に倒れやすいリファクタリング耐性を手放すわけにはいきません。

実質的なトレードオフは「退行への保護」と「素早いフィードバック」の間のスライダーになります。

                    リファクタリング耐性:常に最大化

退行への保護  ◄━━━━━━━━━━ スライダー ━━━━━━━━━━►  素早いフィードバック
(バグ発見力)                                       (実行速度)

テストピラミッド — トレードオフの戦略的配分

このスライダーをどう配分するかの指針が、テストピラミッドという考え方です。
テストの種類ごとの比率を示す概念で、以下のような構造をしています。

        /‾‾‾‾‾‾\
       / E2E     \          ← 少数:退行への保護を重視
      /────────────\
     /  統合テスト    \       ← 中間:保護と速度のバランス
    /──────────────────\
   /   ユニットテスト      \   ← 多数:素早いフィードバックを重視
  /────────────────────────\
  • 底辺(最も多い):ユニットテスト — 実行が速く、素早いフィードバックを重視
  • 中間:統合テスト — 退行への保護と素早いフィードバックのバランスをとる
  • 頂点(最も少ない):E2Eテスト — 退行への保護を重視

どの層でも、リファクタリング耐性は放棄しません。

E2Eテストが少数に留まるのは、実行速度が遅く保守コストも高いため、掛け算での価値が低くなりやすいからです。
最も重要な機能にだけ絞って適用するのが効果的です。

ビジネスロジックが薄いCRUDアプリケーションでは、ユニットテストの価値が下がります。
テストすべきアルゴリズムやビジネスルールが少ないため、トリビアルテストになりやすいのです。
このような場合、統合テスト中心の構成(ピラミッドではなく長方形に近い形)になることもあります。

リファクタリング耐性を確保する実践指針 — ブラックボックステスト

最後に、リファクタリング耐性を確保するための実践的な指針を紹介します。

テストのアプローチには大きく2つの方法があります。

  • ブラックボックステスト:コードの内部構造を知らない前提で、外部から見た振る舞い(入力と出力)だけを検証する
  • ホワイトボックステスト:コードの内部構造を知った上で、分岐やパスを意識して検証する
退行への保護 リファクタリング耐性
ブラックボックステスト 低め 高い
ホワイトボックステスト 高い 低い

リファクタリング耐性は妥協できない柱であるため、テストを「書く」ときはブラックボックスをデフォルトにするのが原則です。

ただし、テストの「分析」にはホワイトボックスの視点を活用できます。
カバレッジツールで「どの分岐がカバーされていないか」を調査し、カバーされていない分岐を見つけたら、その分岐を内部構造を知らないかのようにテストします。

つまり、分析はホワイトボックス、テストの記述はブラックボックスという組み合わせが効果的です。

おわりに

この記事で整理した内容を振り返ります。

  • テストの目的は持続的成長の実現であり、カバレッジの数字よりも1つひとつのテストの質が重要
  • カバレッジは便利な指標だが、質を測る物差しとしては不十分
  • 4本柱(退行への保護・リファクタリング耐性・素早いフィードバック・保守性)が、テストの良し悪しを判断する共通の物差しになる

自分のプロジェクトのテストを1つ選んで、4本柱で評価してみてください。
「このテストは何を守っていて、何を犠牲にしているか」を考えるだけで、テストへの見方が変わるはずです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?