第 25 章 テスト駆動開発のパターン
悪循環の説明
- Gerry Weinberg の**「Quality Software Management」**の概念を引用して、因果ループを説明。
- ストレスレベルが増加するとどうなるか?
- ストレスが増えると、テストをする余裕がなくなる(頻度が減る)。
- テストが減ると、エラーが増える。
- エラーが増えると、さらにストレスが増す。
- これが繰り返され、悪循環に陥る。
気t
テストファーストにすることで生まれる好循環
- 「テストファースト」にすると、逆に良い循環になる
- テストを先に書くルールを決めると、悪循環が良い循環に変わる。
- 「テストファーストをするとストレスが減る」
- 「ストレスが減るともっとテストが書ける」
- 「さらにストレスが減る」
- これが繰り返され、良い循環になる。
- 結果的に、開発がスムーズに進む!
独立したテストの利点
- 各テストの前準備が簡単になり、実行速度が速くなる
- 高凝集、低結合な小さいオブジェクトを設計するようになる
TODO リストの活用方法
- 「すぐやる」・「あとでやる」・「やらない」に分類する
- 新しいタスクが発生したときに、それをすぐにやるべきか、あとでやるべきか、そもそもやらないのかを素早く決める。
- これにより、重要なタスクに集中しやすくなる。
アサーションファースト
- 何事も結論から語ると物事がシンプルになる
- テストで言うなれば、それはアサーションから書くということ
- ということは、読み手もアサーションから読んだ方が理解しやすいのでは?
第 26 章 レッドバーのパターン
「既知から未知へ」という成長の考え方
- すでにある知識を活かしつつ、未知の領域を切り開くことで、開発を進められる。
- 開発とは、「既知」と「未知」の間を行き来しながら成長するプロセスである。
- 「次に TODO リストの中のどれに取り掛かろう?」と悩んだら、「わかりきってはいないが、書けば動きそうな気がするテスト」に取り掛かろう。
学習用テストの主な用途
- 未知の API やフレームワークの動作を理解する
- 例えば、新しいライブラリやフレームワークを使うとき、いきなり本番コードで使うのではなく、まず 学習用テストを書いて、API の挙動を確認 する。
- パッケージやライブラリのアップデート時の動作検証
- 依存しているパッケージやライブラリのバージョンが更新されたとき、まず 学習用テストを実行して、既存の動作が変わっていないかを確認 できる。
- もし、学習用テストが失敗したら、ライブラリの仕様変更が影響している可能性がある。
- その場合、本番コードの修正が必要になるかもしれない。
休憩
- 休憩の重要性
- 疲れたり手詰まりになったときは、休憩を取ることが最善の選択肢。
- 作業から一旦離れることで、新たなアイデアが浮かびやすくなる。
- 例えば、散歩・昼寝・手を洗うなどで頭をリフレッシュできる。
- 席を立った瞬間に「このケースを試していなかった!」と気づくこともある。
- 「シャワーメソッド」
- Dave Ungar が提唱:分からないときはシャワーを浴びる(思考をリセット)。
- テスト駆動開発(TDD)はシャワーメソッドの発展形:
- 何をすべきか分かっているとき → すぐに実装
- まだ分からないとき → 仮実装や三角測量を行う
- それでも分からなければ → シャワーを浴びに行く!
安い机に良い椅子
- 机はいくらでも拡張することができる
- 椅子はとにかく良いものを使おう。
第2 7 章 テスティングのパターン
小さいテスト
- 大きすぎるテストを書いた時には、それをコメントアウトし小さいテストに分割し、仕切り直す。
Mock Object パターン(改善版)
-
📌 なぜ本物の DB を使わず、モックを使うのか?
- テストで本物のデータベース(DB)を使用すると、多くの 外部要因に影響される
- 一貫性のあるテストが難しくなる。
- ネットワーク接続 の状態
- DB サーバーの負荷や可用性
- テストデータのクリアや管理の手間
これらを回避するために、「Mock Object(擬似的な DB)」を使ってテストを行う のが一般的。
- テストで本物のデータベース(DB)を使用すると、多くの 外部要因に影響される
-
📌 Mock Object の適切な使い方
💡 モックをどのように扱うかが重要!🚨 ❌ グローバル変数としてモックを作成しない
- グローバル変数にモックを保持すると、別のテストで意図せず再利用されてしまうリスク がある。
- 例えば、あるテストが mockDb に テストデータ A を設定し、別のテストが mockDb に テストデータ B を設定すると、テスト間で影響が発生し、意図しない動作をする 可能性がある。
- テストの独立性を保つため、グローバル変数ではなくローカル変数として管理するのがベストプラクティス!
-
🚀 ✅ Mock Object はローカル変数として利用する
- 「モックの生成 → 利用 → 破棄」 のサイクルを、各テストごとに独立して行える。
- こうすることで、グローバル変数の影響を受けずにテストが実行できる。
- また、並列実行するテストでも競合の心配がなくなる!
コピーする 編集する @Test public void testOrderLookup() { Database mockDb = new MockDatabase(); // ✅ ローカルスコープでモックを作成 OrderService service = new OrderService(mockDb); // ✅ 依存注入 String result = service.getOrder(123); assertEquals("Mock Order Data", result); // ✅ 独立したテストが可能! }
-
📌 そもそも DB 設計は依存性注入(DI)に対応すべき
- テストと本番の環境を簡単に切り替えられるようにするには、DB の依存関係を直接決めない設計にするのが重要。
- 「DB を直接 new するのではなく、インスタンス生成時に依存先を決める」 ことで、本番用 RealDatabase とテスト用 MockDatabase を簡単に差し替えられる。
コピーする 編集する public class OrderService { private final Database db; // 依存先を固定しない public OrderService(Database db) { // ✅ 依存性注入(DI) this.db = db; } public String getOrder(int orderId) { return db.query("SELECT order_no FROM orders WHERE id = " + orderId); } }
✅ この設計なら、本番では RealDatabase を、テストでは MockDatabase を自由に切り替え可能!
Self Shout
- 「テストの本質ではないが、必要な振る舞い」を簡略化するためのテクニック
- テストケース自身に定義する
- 本質ではないが、オブジェクトを用意するほどでもない部分について適用するのみにとどめるのがポイント
Crash Test Dummy パターン
- エラーの処理を検証したい場合、そのエラーをシュミレートするための方法
- 対象のクラスをオーバーライドし、より簡単にエラーを発するように変更する。
- オーバーライドしたオブジェクトをテストの中で、エラーを発するかを検証するための専用のものとする。
失敗させたままのテスト
- 個人でコーディングする場合の、その日の開発の終わらせ方
- テストをあえて失敗させたまま開発を終える。
- すると、次回再開時に何から取り掛かればよいのか明白になる。
きれいなチェックイン
- チーム開発では、テストが全て通る状態にしてその日の開発を終える。
- 次に作業を行うチームメンバーがフラットな状態で開発に取り掛かれるようにするため
- もし、コーディング終わりのチェックイン時に結合テストに失敗するなら、変更部分はやり直し。
- テストをコメントアウトするなんてもってのほかだ。
第 28 章 グリーンバーのパターン
仮実装を経て本実装へ
- 失敗するテストを書いたら、次は仮実装を行う。
- 仮実装を行ったら、次は本実装を行う。
- 仮実装を挟むことで、例えばテストの記述が誤っており、目指すゴールが間違っていたときも、修正は軽微ですむ。
- つまり、仮実装とはコードの特化、本実装とはコードの一般化である。
三角測量
- 正しい一般化の方向がわからないときは、アサーションを一つ増やす。
- 2つのアサーションと仮実装から正しい一般化を導き出す方法を三角測量という
明白な実装
- 実装の方向性が見えている場合には仮実装を飛ばして本実装を行う場合もある。
- これを明白な実装という
- 明白な実装を行なって、テストがエラーになってしまうなら、ステップを小さく踏み直せばいい
一から多へ
- 既存コードを変更することになった場合には、変更の分離を行う。
- 変更の分離とは、
- 既存コードを保ちつつ、新たなテストケースに対応する実装を行う。
- その後、新規部分が牽制し、既存部分が不必要になった際に、初めて削除する。
- つまり、変更の分離とは、既存部分と新規部分の分離である。
- これにより、安全に変更を加えることができる。
第 29 章 xUnit のパターン
アサーション
- 具体性のあるアサーションをするべき
- 以下ようなアサーションはデバックの効率を高める
- エラーメッセージがついている
- 期待値と実際の値が分かりやすい
- 期待値が絞られている
- ふるまいを検証している
assertEquals("Rectangle area should be 50", 50, rectangle.area());
フィクスチャー
- テストコードに共通の処理があるならば、それはフィクスチャーに抽出することができる
- フィクスチャーに抽出するとコードの重複は減る
- しかし、これが必ずしも全ての開発者にとっていいとは限らない。
- コードをフィクスチャーに抽出するということは、その部分を把握した上でテストコードを読まないといけなくなる。分けることが理解の妨げになることもある。
- フィクスチャーに抽出するべきかどうかは適切に判断するべき
外部フィクスチャー
- テスト後、環境をもとに戻さなけれえばならない
- SetUp()の中で外部リソースを開いたなら、テスト後、必ず閉じなければならない
- テストコード内に書くと、テストの主旨とは関係のない記述が増えてしまうので、TearDown()の中に書く。
- そうすることで、後処理の書き忘れも防げる
テストメソッド
- フィクスチャーが変わるなら、テストメソッドも別のクラスに記述する
- テストメソッドのメソッド名は test から始める。以降は読む人がどんなテストかがわかるように命名する
- テストコードが4行以上になってしまうなら、小さいテストに分割すべき
- テストコードは、後にプロダクトがどのように動くかを保証する契約を務めることになる。その観点を踏まえてテストコードを書くようにする。
例外のテスト
- 適切に例外が発生するかをテストする場合、その記述は通常とことなる
- try 節の中に、処理を書く、catch 節で発生した例外をキャッチする
- 例外が発生しない場合、try 節は次にすすむ。そこに fail()が待っているようにする。
まとめてテスト
- テストスイートには Composite パターンが適用されている
- なので、数種類のテストスイートを大きなテストスイートの中に入れることもできる
- 全てのテストを走らせたいのなら、AllTest というテストスイートを作り、その中に全てを入れればいい。
第 30 章 デザインパターン
Command パターン
- 処理をそのまま記述していくのではなく、オブジェクトの中に記述し、そのオブジェクトを実行することで処理を再現するパターン
- Java のマルチスレッド処理を定義するためのインターフェース Runnable はその典型例
Value Object パターン
- Value Object とは、値を格納するためのオブジェクト
- 値をしたい場合、そのオブジェクト自体の値を帰るのではなく、値を変えた新たなオブジェクトを生成し返す。
- これにより、安全で副作用のない設計が可能となる
Null Object パターン
- NullObject とは
- null のかわりに返される中身のないオブジェクトのこと
- NullObject のメリット
- 受け取り側が毎回 null チェックをする必要がなくなる
- 受け取り側がどんな型のオブジェクトが返ってきたのか気にしなくて済む
- NullObject 使用上の注意点
- null が返っても問題がない場合のみ使用するべき
- null が返ってきて問題があるのならエラー処理をしなければならない。
Template Method パターン
- 親クラスで内容は決めず、処理の順番のみ決めておき
- サブクラスでオーバーライドすることによって、その内容をアレンジする設計手法
- 処理をオブジェクトとして表現するという点では Command パターンと共通している
Pluggable Object パターン
- 条件による処理の違いをオブジェクトで表現する設計パターン
- 条件の違いをオブジェクトの違いに
- 処理の違いをオブジェクトの実装の違いに落とし込む。
- これにより、証券分岐を減らすことができる
Pluggable Selector パターン
-
条件による処理の違いを、動的に条件を取得し、メソッドを選択・実行する設計
-
Method runMethod = getClass().getMethod(this.printMessage); のようにリフレクションを用いて、現在のクラス情報の中から、指定されたメソッドを取得・実行する
-
🔹 目的
- switch 文や if-else の 条件分岐の増殖を防ぐ
- サブクラスの不要な増加を抑える
- 動的にメソッドを選択できるようにする
-
🔹 メリット
- ✅ switch 文を削減し、コードのメンテナンス性を向上
- ✅ クラスの数を増やさず、動的な拡張性を確保
- ✅ 条件が少ない場合に、シンプルで直感的な設計が可能
-
🔹 デメリット
- ⚠ コードの可読性が低下(どのメソッドが呼ばれるか分かりづらい)
- ⚠ リフレクションのオーバーヘッド により、パフォーマンスが低下する可能性
- ⚠ 存在しないメソッドを指定すると NoSuchMethodException が発生する
Factory Method パターン
- インスタンスの生成をコンストラクタではなく、メソッドで行う設計
- これにより、オブジェクト生成のロジックを隠蔽し、統一的な API を提供できる
- コンストラクタによる直接生成では、異なる種類のオブジェクトを追加した際に修正が増える
- 例えば、新しい種類のオブジェクト(例:Euro)を追加する場合、コンストラクタを直接呼び出す設計だと既存コードの修正が必要になる
- Factory Method を用いることで、生成されるインスタンスの型を柔軟に変更できる
- 生成されるインスタンスやその動作を統一でき、コードの再利用性・拡張性が向上する
- 例: Money.dollar(5) や Money.euro(10) のように、インスタンスの生成方法を変更しやすくなる
Imposter パターン
- 既存のコードの設計を大きく変えずに、新しい振る舞いを追加したいときに使う
- ポリモーフィズム の応用により、異なる動作をするクラスを同じプロトコルで扱える
- 「この新しい振る舞いは今のコードでは実現できない!」→「新しいクラスを作ろう!」という閃き から生まれる
- 結果として 「既存コードを変更せずに拡張」 する手法となる
- 条件分岐や重複コードを減らすことができる
- 新しいバリエーション(機能)を導入しやすい
Composite パターン
- Composite パターンとは
- 個(単体のオブジェクト)とその集合(複数のオブジェクトのグループ)を同じように扱うための設計
- 個と集合が共通のインターフェース(または親クラス)を持つことで、一貫した操作ができる
Collecting Parameter パターン
- 情報を集める専門のオブジェクト
- 例えば Composite パターンを表す一つの会社があるとする
- 全社員の年齢を集約するために、一人一人が発言する自身の年齢を誰かが聞き取って集約していたらミスが発生しやすいし、処理が複雑である。
- しかし、全社員のもとに名簿(Collecting Parameter) が回ってきて、一人一人がこれに記入をするだけならば、処理がシンプルになる。
第 31 章 リファクタリング
差異をなくす
-
仮実装されたものを本実装に導くものがリファクタリングである
-
リファクタリングでは本実装で記述された特化されたコードを一般化していく
-
その過程では以下のような様々なコードの重複が削除されていく
-
ループの統一(マージ)
- 似たようなループが 2 つある場合、それらの処理が完全に一致するようにリファクタリングすれば、1 つにまとめられる。
- 例えば、異なる配列をそれぞれ処理するループがあったとして、それらの配列の型や処理内容を統一することで、1 つのループにまとめられる。
-
条件分岐の統一
- if 文や switch 文などの条件分岐が似ている場合、それらを完全に一致させることで、1 つの分岐に統合できる。
- 例えば、if (x == 1) {処理 A} と if (x == 1) {処理 B} のようなものがあれば、処理 A と B を統一することで、条件分岐を削減できる。
-
メソッドの統一
- 似たようなメソッドが 2 つある場合、それらを完全に一致させることで、1 つに統合できる。
- 例えば calculateSum() と computeTotal() のようなメソッドがあったとして、どちらも内部で似たような処理をしているなら、それらを統一することで冗長なコードを削減できる。
-
クラスの統一
- 似たようなクラスが 2 つある場合、それらを完全に一致させることで、1 つに統合できる。
- 例えば User クラスと Customer クラスが非常に似ていて、Customer には User にはない数個のプロパティがあるだけなら、User クラスを拡張する形に変更するか、1 つに統合することで、設計がシンプルになる。
-
変更の分離
- 一つのメソッドの内部処理を変更する場合、そのメソッドの変更必要箇所と、そうでない部分を切り分け、変更必要箇所のみを変更していく。
- これにより、変更部分以外への影響をなくす。
データ構造の変更
-
🔵 「内側から外側へ変更する方法」
- 先にデータ構造を変更し、後でその利用部分を修正していく
- 先に厨房を変更し、後からお客様向けメニューを変える。
-
🔴 「API(外部)から変更する方法」
- 先に API のリクエスト形式を変更し、後でその内部処理を変える。
- 先にお客様向けメニューを変え、段階的に厨房の仕組みを移行する。
-
どちらの方法も「最終的にシステムを移行する」ことが目的だが、以下の使い分けをすると、スムーズな移行が可能になる
- 内部の混乱を防ぎたいなら → 内部を先に変更する(内側 → 外側)
- 外部システムとの互換性を維持したいなら → 外部を先に変更する(外側 → 内側)
メソッドのインライン化
- メソッドの制御フローが散らかってしまった場合、一度メソッドをインライン化し、何がどうなっているのかを精里してから再度リファクタリングしてみるのもよい。
メソッドオブジェクト
- 抽出部分をオブジェクトにする方法
- 必要な引数やパラメータをインスタンス変数として保持し、処理は run()によって実行される
第 32 章 TDD を身につける
一歩の大きさはどれくらいか
- テストは細かく書いていくことが基本
- リファクタリングも、細かく分割して行う
テストしなくてよいものはあるか
- 不安が退屈に変わるまでテストを書く
- 他人が書いたコードのテストはしないこと
- ただし、他人が書いたコードに対する学習テストは書くことがある
良いテストを見分けるkとおができるのか
- テストを書くことが設計の悪さを炙り出すことにつながることがある
- 前準備が長い、前準備が重複している、テスト実行時間が長い、テストが思わぬところで失敗する などはプロジェクトの設計に問題があることを示唆する
TDD はどのようにフレームワークを導くか
-
TDD では、テストをクリアするための最小限のコードを書く
- 最初の実装はシンプルで、無駄のないものになる。
- 汎用的な設計を最初から考えるのではなく、実際に必要になったときに拡張していく。
-
追加機能が必要になると、新しいテストを書き、それを満たすようにコードを修正
- このとき、Imposter パターンなどを適用し、新しい機能を追加しやすくする。
- 既存のコードは壊さず、修正ではなく拡張によって進化する。
-
結果として、開放閉鎖原則(OCP)が自然に満たされる
- 「オブジェクトは利用に対して開かれ、修正に対して閉じられる」状態になる。
- 既存の機能を壊さずに、新しいバリエーションを追加しやすい設計になる。
どれくらいのフィードバックが必要か
- どれくらいのテストを書くことが正解だろうか?
- それはプロダクトに必要な自信の大きさによる
- ペースメーカーのような絶対に壊れてはならないものは、絶対的な自信が得られるだけのテストが必要
- この必要な自信の大きさは平均故障間隔 MTBF でもある
どのようなときにテストを消すべきか
- 消しても自信が失われないのであれば減らしてもよい
- 消しても読み手の理解が損なわれないのであれば消してもよい
プログラミング言語や環境は TDD に影響するか
- TDD のサイクル(テスト・コンパイル・実行・リファクタリング)が面倒な言語や環境だと、どうしてもスモールステップを避けてしまう。
- TDD のサイクルを回しやすい言語や環境だと、試行錯誤をしやすい。結果的に開発効率が上がる。
巨大なシステムをテスト駆動できるか
- 巨大なシステムであろうが、小さなシステムであろうが、一つ一つの機能は独立しているべきである。
- そのような高凝集低結合な設計のもとでは、特定の機能の開発の過程で全てのテストを走らせる必要はない
- 高凝集低結合な設計のもとでは、TDD を適用することができる
アプリケーションレベルのテストで開発を駆動できるのか
-
アプリケーションレベルのテストで開発を駆動する
- メリット
- 顧客目線のテストを書くことにより、顧客のニーズから逸脱することを防ぐ
- 別の視点のテストが含まれることによりバランスの取れた開発が可能になる
- デメリット
- チーム全体がこれを受け入れることに納得しなければならない
- テストの追加からグリーンバーまでが長くなる。
- そのため、グリーンバーに至るまでに仕様変更などが入る可能性がある
- メリット
-
アプリケーションレベルのテストに比べて、ユニットテストは個人で実践できる利点がある。これを実践していくことで、まずはチームに TDD の文化を浸透させよう。
途中から TDD に乗る換えるにはどうすればよいか
- テストのことを考えずに書かれたコードはテストが書きにくいものだ
- したがって、既存のコードに対してテストを書こうとすると、コードそのものを書き直したくなってしまう。
- しかし、機能の追加もせずに、テストを書くこととリファクタリングばかりやっていても金は生まれない。
- 大切なのは、スポットを絞ること。
- 機能の追加が影響を及ぼす範囲はどこなのか?
- その範囲でなら既存のコードへのテストの追加やフィファクタリングもすればよい
TDD は誰のためのものか
- TDD はより良いコードを書けば、よりうまくいく と信じているエンジニアのためのものだ
- TDD を行えば、正しいタイミングで正しい問いに気づけるようになる。より綺麗な設計になり、学びを得るに従って設計を改善できる。
- プロダクトは大きな夢とともに始まる。しかし、開発が進むにつれ、次第にコードが悪臭が放つようになり、キラキラとした輝きは失われていく
- しかし、TDD はプロダクトが当初もっている大きな夢を潰さない。プロダクト全体のコードをクリーンなものに保ってくれる。
TDD は初期状況に左右されるか
- 数あるタスクのうち、どのダスクにどの順番で取り掛かっていくかによって TDD の進め方は変わっていく
- これ次第では、開発効率が上がることもあれば、下がることもある
- しかし、多いな視野で見た場合、それは誤差の範囲である
- TDD を適切に実践すれば、最終的な成果は大きく変わらない
TDD とパターンの関係
- 反復可能な振る舞いはパターン化するべきである
- そうして浮いた思考のリソースを例外的な状況や、規則に当てはまらない問題に充てるべきである
- プロダクトの設計すらもパターン化可能なものであり、TDD はそれを実践している
- 自力で慎重に考え抜いた設計も、TDD によるパターンのもとでなされた設計には劣る
なぜ TDD は機能するのか
- 欠陥とコストの削減
- 欠陥を早期に発見修正できる
- これにより、開発コストも削減できる
- 安心して開発できる環境が構築されるのでチームの信頼関係も向上する
- フィードバックループの短縮
- 数分単位の改善を繰り返すことができる
- 短いループを永続的に回すことによりアトラクター理論が働く
- アトラクター理論
- 一定の影響力が与えられ続けるならば、システムの初期状態がどこにあったとしても、最終的に特定の軌道や状態へ収束する。
- TDD という小さいサイクルによる設計の改善の力が働き続ける限り、そのプロダクトの設計は良い形へと収束していく
名前の由来は
- TDD はテスト駆動
- では、テスト駆動ではないなら、何によって駆動されるのか?
- 憶測だろうか?
- 設計や実装を、推測や経験則に基づいて進める開発スタイル。
- 開発者は「こうすればうまく動くだろう」「この機能は必要になるはずだ」と先入観でコードを書き進める。
- その結果、実際に動かしてみるとバグが発生したり、実際には不要な機能を作り込んでしまう可能性がある。
- 仕様だろうか?
- つまり、ウォーターフォール型開発
- 仕様は変更されることがある - 完璧な仕様などない
- 柔軟性が失われる
- だからテスト駆動開発
- 使用の代わりにテストを書く
- 憶測ではなく、テストの結果という事実を頼りに開発する
- 変更に強い開発
TDD と XP(eXtreme Programming)の関係
-
TDD が強化する XP のプラクティス
-
ペアプログラミング
-
TDD のテストが議論の媒介となり、問題解決がスムーズになる。
-
いきいきとした仕事
-
XP の「疲れたらやめる」文化と相性が良く、負担を減らせる。
-
継続的インテグレーション
-
短いテスト・コミットのサイクルを繰り返し、品質を維持。
-
インクリメンタルな設計
-
必要なコードのみを記述し、不要な冗長性を排除。
-
リファクタリング
-
システムの振る舞いを保証しながら、安心してコード改善が可能。
-
継続的デリバリー
-
頻繁なデプロイでリスクを低減し、システムの安定性を向上。
-
-
結論
- TDD は XP の他のプラクティスと連携し、開発の生産性と品質を向上させる。
- 頻繁なテストとフィードバックにより、持続可能な開発を実現する。