POODR 第9章:コスト効果の高いテストを設計する

Tony Duong

Tony Duong

7月 17, 20262

他の言語:🇫🇷🇬🇧
#poodr#ruby#object-oriented-design#testing#tdd#sandi-metz
POODR 第9章:コスト効果の高いテストを設計する

概要

第9章は、テストをそれ自体が設計問題として扱うことで本書を締めくくります。テストはタダではありません — 書く、読む、変更するのに時間がかかります。良いスイートは、アプリケーションが動作し*、*設計が健全であることを証明しつつ、コードが進化しても保守コストを抑えられます。目標はコスト効果の高いテストです — 継続的なコストを最小に、確信を最大に。

テストの意図

テストにはいくつか重なり合う目的があります。

  • バグを見つける — ユーザーより先にリグレッションを捕まえる。
  • 設計を文書化する — よく書かれたテストは、オブジェクトの使い方の実行可能な例である。
  • 設計判断を先送りする — 安全網があれば、恐れずにより良い設計へリファクタリングできる。
  • 変更を支える — スイートはアプリを変更するために存在する。テストを変える方がコードを変えるより難しければ、スイートは負債になっている。

テストの目的は、最も多くのテストを書くことではない — 正しいテストを書くことだ。

何をテストするか

各オブジェクトの公開インターフェースをテストする。オブジェクトの契約を構成する incoming メッセージが重要である。次はテストしない。

  • private メソッド — 実装の詳細である。これらをテストすると、スイートがオブジェクトの働き方に結合され、内部リファクタリングのたびに、公開契約について追加で何も証明しないテストが壊れる。
  • 状態を変えず有用な値も返さない incoming メッセージ — 通常はアサートする価値がない。
  • 階層の各層や重なるロールで同じ振る舞いを二度 — アサーションは振る舞いが定義されている場所に、一度だけ置く。

有用な経験則:壊れうるものはすべてテストするが、一度だけ、公開インターフェースを通じてのみ。

いつテストするか

コードを書くタイミングにできるだけ近くテストを書く — 理想的には先に(TDD)、または直後に。後から長く経って書いたテストは正しく書きにくい(意図したインターフェースを覚えていない)し、偶発的な設計を固定しがちである。

また:これから変更するコードのテストを書く。 触る必要のある未テスト領域は、まずスイートを拡張すべき場所であり、変更を検証できるようにする。

どうテストするか

Incoming メッセージ

値を返す公開メソッド(query)ごとに、戻り値をアサートする。状態を変える公開メソッド(command)ごとに、結果の状態をアサートする。

# Incoming query — assert the result
assert_equal 4.727, gear.gear_inches

# Incoming command — assert the side effect / resulting state
bicycle.shift
assert_equal :high, bicycle.gear

これらのテストは、オブジェクトが送信者との契約を守ることを証明する。

Outgoing メッセージ

outgoing メッセージは2種類に分かれる。

  1. 他者へ送る query(値を求めて使う) — メッセージが送られたことをアサートしない。戻り値は incoming メッセージのテストですでに検証されている。送信をアサートすると、協調者の private な詳細に結合される。
  2. 他者へ送る command(別オブジェクトに何かをさせ、副作用が重要) — メッセージが送られたことをアサートする。通常は mock を使う。command が発火しなければ、このオブジェクトが単体では正しく見えても、世界の残りは誤っている。
# Outgoing command — prove the message is sent
mock_wheel = MiniTest::Mock.new
mock_wheel.expect(:rotate, true)
gear = Gear.new(chainring: 52, cog: 11, wheel: mock_wheel)
gear.rotate_wheel
mock_wheel.verify

duck type のテスト

複数クラスがロール(duck type)を演じるとき、そのロール用の共有テストを書き、各プレイヤーのテストに含める。こうしてロールのインターフェースは一度だけ文書化され、すべてのプレイヤーがそれを守ることが証明される。

継承コードのテスト

  • 抽象スーパークラスを、テスト専用の具象サブクラス(またはすべての実サブクラス)経由でテストする。
  • 各サブクラスでは特殊化 — 継承デフォルトと異なるフックとオーバーライド — だけをテストする。スーパークラスのスイートがすでにカバーする継承振る舞いを再テストしない。

test double を賢く使う

stub は固定値を返し、テスト対象オブジェクトを分離する。mock は stub に加え、特定のメッセージ(特定の引数付き)が送られたことをアサートする。mock の使いすぎは、協調者のインターフェースが変わるたび — 振る舞いがまだ正しくても — 壊れる脆いテストを生む。

次を優先する。

  • 安く安定しているなら、実際のシンプルな協調者に対してテストする。
  • mock は、副作用を検証しなければならないoutgoing commandにだけ使う。
  • ロールテスト(共有例)で、duck type の各プレイヤーを同じ方法で確認する。

テストの変更コストを抑える

前章までの設計ルールはスイートにも当てはまる。

  • 実装ではなくインターフェースに依存する — 結果と公開契約をアサートし、private メソッド呼び出しや内部データ構造はアサートしない。
  • 重複を避ける — 共有ロールテスト、オブジェクトセットアップ用の factory/fixture。
  • 意図を明確にする — 失敗したテストは、壊れた契約をはっきり指し示すべきである。

よく設計されたアプリケーションを反映するスイートは、小さく明確で更新しやすい。private な詳細に手を伸ばすスイートは、もう一つの絡み合った依存グラフになる。

要点

  1. テストは投資である — カバレッジ率だけでなく、保守コストあたりの確信を最適化する。
  2. 公開インターフェースをテストする — incoming query(結果をアサート)と incoming command(結果の状態をアサート)。
  3. private メソッドはテストしない;各サブクラス/プレイヤーで継承・共有振る舞いを再テストしない。
  4. outgoing query には「送られたか?」アサーションは不要outgoing command には必要(mock を使う)。
  5. duck type ロールは共有テストで文書化し、各プレイヤーがそれを含める。
  6. コードの近くでテストを書く(TDD または直後)、特に未テスト領域を変更する前に。
  7. 良いスイートは変更を支える — テストの方がコードより変更しにくければ、テストを再設計する。

これは Practical Object-Oriented Design in Ruby の最終章である。本書全体は、変更を安く保てるようにオブジェクトを配置する実践的ガイドであり、第9章はその規律を設計を守るテストへと広げる。


🌐 Claudeによる翻訳

Tony Duong

著者: Tony Duong

デジタル日記。思考、経験、そして人生についての考え。