0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リファクタリングでテストが壊れる本当の理由

0
Posted at

はじめに

メソッドを抽出しただけ。
ループをStreamに書き換えただけ。
機能は何も変えていない。

それなのに、テストが真っ赤になる。
――そんな経験はありませんか?

これは 偽陽性(false positive) と呼ばれる現象です。
実際にはバグがないのに、テストが「バグがある」と報告してしまう。
いわばテストの「誤報」です。

偽陽性が少ないテストの性質をリファクタリング耐性と呼びます。
リファクタリング耐性は、良いテストを構成する4つの柱の1つであり、
テストスイートの信頼性を左右する重要な指標です。

この記事では、偽陽性がなぜ起きるのか、
その根本原因と「何をテストすべきか」の判別基準を整理していきます。

TL;DR

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

セクション1: 偽陽性はなぜテストスイートを殺すのか

偽陽性が「たまに壊れるテスト」程度の問題なら、
手で直せば済む話です。
でも実際には、もっと深刻な悪循環を引き起こします。

偽陽性が生む悪循環

最初は「どうせまた偽陽性だろう」で始まります。
テスト結果を真剣に見なくなり、やがて本物のバグまで素通りさせてしまう。

テストが信用できないから、リファクタリングも怖くてできなくなる。
コードは手を入れられないまま劣化していき、さらに壊れやすいテストが増えていく。

偽陽性の問題は「テスト1件の修正コスト」ではありません。
テストスイート全体への信頼が崩壊する、チームレベルの問題です。

この悪循環を断ち切るには、偽陽性の根本原因を知る必要があります。

セクション2: 「観察可能な振る舞い」と「実装詳細」

偽陽性の原因は「実装詳細への結合」

テストが偽陽性を出すのは、
リファクタリングで変わる部分に結合しているからです。

リファクタリングとは、外部から見た振る舞いを変えずに
内部構造を改善する行為です。
つまりリファクタリングで変わるのは「内部構造」、
変わらないのは「外部から見た振る舞い」。

ここから自然に2つの分類が見えてきます。

  • リファクタリングで変わる部分 = 実装詳細
  • リファクタリングで変わらない部分 = 観察可能な振る舞い

実装詳細に結合しているテストは、
リファクタリングのたびに壊れます。
観察可能な振る舞いだけを検証していれば、
偽陽性は原理的に発生しません。

では、この2つをどうやって見分けるのでしょうか?

判別基準: 「クライアントのゴールに直結しているか?」

コードが「観察可能な振る舞い」であるためには、
次のどちらかを満たす必要があります。

  • クライアントのゴール達成に直結する操作を公開している
    (計算を行う、副作用を起こす、など)
  • クライアントのゴール達成に直結する状態を公開している
    (システムの現在の状態を返す、など)

上記のどちらにも該当しないコードは、すべて「実装詳細」です。

ここでの「クライアント」とは、そのコードを呼び出す側のことを指します。
同じコードベース内の呼び出し元クラス、外部アプリケーション、UIなど、
文脈によって変わります。

もう1つ、重要な注意点があります。
「publicかprivateか」と
「観察可能な振る舞いか実装詳細か」は別の軸です。

publicなメソッドであっても、
クライアントのゴールに直結していなければ実装詳細になりえます。

操作の具体例で理解する

判別基準を具体例で見てみましょう。

あるECサイトで、商品名を変更する機能を考えます。
Productクラスには2つのpublicメソッドがあるとします。

┌──────────────────────────────────┐
│ Product (publicメソッド)          │
├──────────────────────────────────┤
│ + setName(newName)               │
│ + sanitizeName()                 │
├──────────────────────────────────┤
│ 不変条件: 商品名は50文字以内         │
└──────────────────────────────────┘

ProductService(クライアント)のゴールは
「商品名を変更すること」です。

// ProductService(クライアント)のコード
public void renameProduct(long productId, String newName) {
    Product product = repository.findById(productId);
    product.setName(newName);
    product.sanitizeName(); // 50文字制限を適用
    repository.save(product);
}

判別基準に当てはめてみます。

  • setName() → クライアントのゴール「商品名の変更」に直結
    観察可能な振る舞い
  • sanitizeName() → クライアントのゴールに直結しない。
    50文字制限という内部の不変条件を満たすための手段にすぎない
    実装詳細

sanitizeName()がpublicであること自体が問題です。
これが「実装詳細の漏洩」と呼ばれる状態です。

状態の具体例で理解する

もう1つ、状態の例も見てみましょう。

ReportGeneratorクラスが、
複数のセクションを組み合わせてレポートを生成するとします。

┌──────────────────────────────────────┐
│ ReportGenerator (publicメンバー)      │
├──────────────────────────────────────┤
│ + generate(): String                  │
│ + getSections(): List<Section>        │
└──────────────────────────────────────┘

クライアントのゴールは
「レポートを生成すること」です。

  • generate() → クライアントのゴールに直結
    観察可能な振る舞い
  • getSections() → クライアントのゴールに直結しない。
    内部でどんなセクションに分割しているかは実現方法の話
    実装詳細

getSections()の中身(セクション数や構成)を
テストで検証してしまうと、
内部構造をリファクタリングしたときにテストが壊れます。
検証すべきはgenerate()の出力結果だけです。

判別のヒューリスティック

ここまでの例から、実用的な経験則が1つ見えてきます。

クライアントが1つのゴールを達成するために、
呼び出す操作が2つ以上ある場合、
実装詳細が漏洩している可能性が高い。

理想的には、1つのゴールは1つの操作で達成できる状態が望ましいです。

先ほどのProductServiceの例では、
setName()sanitizeName()の2つを呼ぶ必要がありました。
これは実装詳細の漏洩のサインです。

セクション3: API設計とテスト設計は表裏一体

判別基準はわかりました。
でも「実装詳細をテストしないように気をつける」だけでは、
意志の力に頼った解決策です。
もっと構造的に防ぐ方法があります。

well-designed API ── 実装詳細をテストできなくする

セクション2で見た判別基準を思い出してください。
publicかprivateかと、
観察可能な振る舞いか実装詳細かは別の軸でした。

この2つの軸を組み合わせると、
API設計の良し悪しが見えてきます。

              │ 観察可能な振る舞い   │   実装詳細
 ─────────────┼───────────────────┼──────────────
  public      │    良い設計        │  悪い設計
              │                   │ (実装詳細の漏洩)
 ─────────────┼───────────────────┼──────────────
  private     │    該当なし(※)     │  良い設計
              │                   │

※ privateにした時点でクライアントが直接使えなくなるため、
「クライアントのゴールに直結する」という条件を満たせません。

「観察可能な振る舞い = public」かつ
「実装詳細 = private」になっている状態が、
well-designed APIです。

well-designed APIにすれば、テストは物理的に
観察可能な振る舞いしか検証できなくなります。
つまり「テストで何を検証すべきか」の問題は、
「APIをどう設計すべきか」の問題と同じなのです。

先ほどのProductクラスを改善してみましょう。

public class Product {
    private String name;

    public void setName(String newName) {
        this.name = sanitize(newName); // 内部でサニタイズ
    }

    public String getName() {
        return this.name;
    }

    // privateに変更 ── クライアントから見えなくなる
    private String sanitize(String input) {
        if (input.length() > 50) {
            return input.substring(0, 50);
        }
        return input;
    }
}

sanitize()がprivateになったことで、
クライアントはsetName()の1回の呼び出しだけで
ゴールを達成できるようになりました。
テストもsetName()の結果だけを検証すれば十分です。

カプセル化 ── 不変条件を守る仕組みとしての再理解

well-designed APIを維持することは、
カプセル化の考え方と深く関連しています。

カプセル化というと「データを隠蔽すること」と
理解されがちですが、
本質はコードを不変条件の違反から守る行為です。

改善前のProductクラスを振り返ってみます。

sanitizeName()がpublicだと、
クライアントがsetName()だけ呼んで
sanitizeName()を呼び忘れる可能性があります。
すると「商品名は50文字以内」という不変条件が破られます。

改善前: 不変条件の違反が「できてしまう」
─────────────────────────────────────
  client → product.setName("長い名前...")
         → // sanitizeName()を呼び忘れ
         → 50文字を超えた名前がDBに保存される
改善後: 不変条件の違反が「できない」
─────────────────────────────────────
  client → product.setName("長い名前...")
         → 内部でsanitize()が自動実行
         → 必ず50文字以内になる

実装詳細を隠蔽し、操作とデータを束ねることで、
不変条件違反の可能性自体を排除する。
これがカプセル化の効果です。

カプセル化もテスト設計も、根底にある考え方は同じです。
「正しくないことができる可能性自体をなくす」

意志の力で「気をつける」のではなく、
構造で「間違えようがない状態」を作ること。
API設計とテスト設計は、この同じ原則の表と裏です。

セクション4: システムの境界で判断する

ここまではクラスレベルの判別基準を見てきました。

  • セクション2: メソッド単位で「観察可能な振る舞い」と「実装詳細」を判別する
  • セクション3: API設計で実装詳細を構造的に隠蔽する

でも実際のアプリケーションは、
多数のクラスが協調して動いています。
この判別基準を、
システム全体のレベルにどう拡張すればいいのでしょうか?

ヘキサゴナルアーキテクチャの概要

典型的なアプリケーションは、
大きく2つの層で構成されます。

┌───────────────────────────────────────────┐
│         アプリケーションサービス層            │
│    (外部世界との橋渡し・ユースケース)         │
│                                           │
│   ┌───────────────────────────────────┐   │
│   │          ドメイン層                 │   │
│   │     (ビジネスロジック)             │   │
│   └───────────────────────────────────┘   │
│                                           │
└───────────────────────────────────────────┘
  • ドメイン層: ビジネスロジックを持つ中心部分
  • アプリケーションサービス層: ドメイン層と外部世界を繋ぐ層。
    DBからデータを取得してドメインオブジェクトに渡し、
    結果をDBに保存したり外部サービスに送信したりする

この2つの層がひとまとまりの「ヘキサゴン(六角形)」を形成します。

他のアプリケーション
(メール送信サービス、メッセージバス、外部APIなど)も
それぞれ独自のヘキサゴンとして表現され、
このヘキサゴン同士の相互作用の構造が
ヘキサゴナルアーキテクチャです。

ポイントは3つあります。

  1. 関心の分離: ドメイン層はビジネスロジックだけに責任を持ち、
    外部との通信はアプリケーションサービス層が担う
  2. 依存の方向は一方向: アプリケーションサービス層 → ドメイン層。
    ドメイン層は外部世界を知らない
  3. 外部との接続口: 外部アプリケーションは
    アプリケーションサービス層を通じて接続する。
    ドメイン層に直接アクセスすることはない

ここで大切なのは、
セクション2で学んだ判別基準が
各層にもそのまま適用できるということです。

ドメイン層にも「観察可能な振る舞い」と「実装詳細」があり、
アプリケーションサービス層にもそれがある。
判別基準はフラクタル的に、どのレベルにも使えます。

2種類の通信 ── intra-systemとinter-system

ヘキサゴナルアーキテクチャの視点を持つと、
アプリケーション内の通信は2種類に分類できます。

                外部アプリケーション
                (メールサービス等)
                      ▲
                      │ inter-system通信
                      │ (アプリケーション間)
┌─────────────────────┼──────────────────────┐
│  アプリケーション      │                      │
│  サービス層           │                      │
│         ┌───────────┘                      │
│         │                                  │
│         ▼ intra-system通信                  │
│  ┌─────────────┐                           │
│  │ ドメイン層    │                           │
│  └─────────────┘                           │
└────────────────────────────────────────────┘
  • intra-system通信:
    アプリケーション内部のクラス間の通信
  • inter-system通信:
    アプリケーションと外部アプリケーションとの通信

そして、ここが核心です。

intra-system通信は実装詳細。
inter-system通信は観察可能な振る舞い。

なぜでしょうか?
セクション2の判別基準をそのまま当てはめてみます。

intra-system通信

あるドメインクラスが
別のドメインクラスをどう呼び出すかは、
クライアントのゴール達成に直結しません。
それは内部でどう実現するかという話であり、実装詳細です

inter-system通信

アプリケーションが外部システムとどう通信するかは、
外部から観察可能な振る舞いの一部です。
外部システムはその通信パターンを観察し、依存しています。
また、外部システムは自分のアプリケーションと同時にデプロイできないことがあるため、
通信のインターフェースは後方互換性を維持する必要があります

具体例: 商品購入のユースケース

ECサイトで商品を購入するユースケースで考えてみましょう。

┌────────────────────────────────────────────────┐
│ 自分のアプリケーション                             │
│                                                │
│  OrderService                                  │
│    │                                           │
│    ├── inventory.decrease(productId, quantity) │
│    │     ↕ Inventory(ドメインクラス)            │
│    │     → intra-system通信 → 実装詳細           │
│    │                                           │
│    └── emailGateway.sendReceipt(order)         │
│          ↕ 外部メールサービス                     │
│          → inter-system通信 → 観察可能な振る舞い   │
│                                                │
└────────────────────────────────────────────────┘
  • Inventoryクラスのdecrease()を呼ぶのは
    intra-system通信です。
    在庫の減算をどのクラスのどのメソッドで実現するかは、
    内部の実装方法の問題です。
    リファクタリングでInventoryProduct
    統合しても、外部から見た振る舞いは変わりません
  • EmailGateway経由でメール送信するのは
    inter-system通信です。
    「購入したら確認メールが届く」という振る舞いは、
    外部のクライアントから観察可能であり、
    維持すべき契約の一部です

この分類がテスト設計に与える影響

intra-system通信が実装詳細であるという事実は、
テスト設計に直接的な影響を与えます。

テストで「クラスAがクラスBのメソッドXを呼んだか」を
検証している場合、それはintra-system通信、
つまり実装詳細を検証していることになります。

内部のクラス構成をリファクタリングしたら壊れる、
脆弱なテストです。

この通信の分類は、
テストダブル(Mock、Stubなど)の使い方にも直結します。
どの通信をMockで検証すべきかという判断基準は、
まさにこのintra/interの区別に基づいています。

おわりに

この記事を通じて見てきたのは、
たった1つの問い――
「クライアントのゴールに直結しているか?」
――がスケールを変えながら繰り返し現れるということでした。

メソッド単位の判別も、API設計の良し悪しも、
システム間通信の分類も、
すべてこの同じ問いから導き出せます。

テストを書く前に
「これは観察可能な振る舞いか、実装詳細か?」と自問する。
この習慣が、偽陽性を防ぎ、
テストスイートへの信頼を守ってくれるはずです。

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?