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?

テスト駆動開発 第3部 テスト駆動開発のパターン

Last updated at Posted at 2025-02-25

第 25 章 テスト駆動開発のパターン

悪循環の説明

  • Gerry Weinberg の**「Quality Software Management」**の概念を引用して、因果ループを説明。
  • ストレスレベルが増加するとどうなるか?
    1. ストレスが増えると、テストをする余裕がなくなる(頻度が減る)。
    2. テストが減ると、エラーが増える。
    3. エラーが増えると、さらにストレスが増す。
    4. これが繰り返され、悪循環に陥る。
      気t

テストファーストにすることで生まれる好循環

  • 「テストファースト」にすると、逆に良い循環になる
  • テストを先に書くルールを決めると、悪循環が良い循環に変わる。
    1. 「テストファーストをするとストレスが減る」
    2. 「ストレスが減るともっとテストが書ける」
    3. 「さらにストレスが減る」
    4. これが繰り返され、良い循環になる。
  • 結果的に、開発がスムーズに進む!

独立したテストの利点

  • 各テストの前準備が簡単になり、実行速度が速くなる
  • 高凝集、低結合な小さいオブジェクトを設計するようになる

TODO リストの活用方法

  • 「すぐやる」・「あとでやる」・「やらない」に分類する
  • 新しいタスクが発生したときに、それをすぐにやるべきか、あとでやるべきか、そもそもやらないのかを素早く決める。
  • これにより、重要なタスクに集中しやすくなる。

アサーションファースト

  • 何事も結論から語ると物事がシンプルになる
  • テストで言うなれば、それはアサーションから書くということ
  • ということは、読み手もアサーションから読んだ方が理解しやすいのでは?

第 26 章 レッドバーのパターン

「既知から未知へ」という成長の考え方

  • すでにある知識を活かしつつ、未知の領域を切り開くことで、開発を進められる。
  • 開発とは、「既知」と「未知」の間を行き来しながら成長するプロセスである。
  • 「次に TODO リストの中のどれに取り掛かろう?」と悩んだら、「わかりきってはいないが、書けば動きそうな気がするテスト」に取り掛かろう。

学習用テストの主な用途

  • 未知の API やフレームワークの動作を理解する
    • 例えば、新しいライブラリやフレームワークを使うとき、いきなり本番コードで使うのではなく、まず 学習用テストを書いて、API の挙動を確認 する。
  • パッケージやライブラリのアップデート時の動作検証
    • 依存しているパッケージやライブラリのバージョンが更新されたとき、まず 学習用テストを実行して、既存の動作が変わっていないかを確認 できる。
    • もし、学習用テストが失敗したら、ライブラリの仕様変更が影響している可能性がある。
      • その場合、本番コードの修正が必要になるかもしれない。

休憩

  1. 休憩の重要性
    • 疲れたり手詰まりになったときは、休憩を取ることが最善の選択肢。
    • 作業から一旦離れることで、新たなアイデアが浮かびやすくなる。
    • 例えば、散歩・昼寝・手を洗うなどで頭をリフレッシュできる。
    • 席を立った瞬間に「このケースを試していなかった!」と気づくこともある。
  2. 「シャワーメソッド」
    • Dave Ungar が提唱:分からないときはシャワーを浴びる(思考をリセット)。
    • テスト駆動開発(TDD)はシャワーメソッドの発展形:
      • 何をすべきか分かっているとき → すぐに実装
      • まだ分からないとき → 仮実装や三角測量を行う
      • それでも分からなければ → シャワーを浴びに行く!

安い机に良い椅子

  • 机はいくらでも拡張することができる
  • 椅子はとにかく良いものを使おう。

第2 7 章 テスティングのパターン

小さいテスト

  • 大きすぎるテストを書いた時には、それをコメントアウトし小さいテストに分割し、仕切り直す。

Mock Object パターン(改善版)

  1. 📌 なぜ本物の DB を使わず、モックを使うのか?

    • テストで本物のデータベース(DB)を使用すると、多くの 外部要因に影響される
      • 一貫性のあるテストが難しくなる。
      • ネットワーク接続 の状態
      • DB サーバーの負荷や可用性
      • テストデータのクリアや管理の手間

    これらを回避するために、「Mock Object(擬似的な DB)」を使ってテストを行う のが一般的。

  2. 📌 Mock Object の適切な使い方
    💡 モックをどのように扱うかが重要!

    🚨 ❌ グローバル変数としてモックを作成しない

    • グローバル変数にモックを保持すると、別のテストで意図せず再利用されてしまうリスク がある。
    • 例えば、あるテストが mockDb に テストデータ A を設定し、別のテストが mockDb に テストデータ B を設定すると、テスト間で影響が発生し、意図しない動作をする 可能性がある。
    • テストの独立性を保つため、グローバル変数ではなくローカル変数として管理するのがベストプラクティス!
  3. 🚀 ✅ 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); // ✅ 独立したテストが可能!
    }
    
  4. 📌 そもそも 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 章 リファクタリング

差異をなくす

  • 仮実装されたものを本実装に導くものがリファクタリングである

  • リファクタリングでは本実装で記述された特化されたコードを一般化していく

  • その過程では以下のような様々なコードの重複が削除されていく

    1. ループの統一(マージ)

      • 似たようなループが 2 つある場合、それらの処理が完全に一致するようにリファクタリングすれば、1 つにまとめられる。
      • 例えば、異なる配列をそれぞれ処理するループがあったとして、それらの配列の型や処理内容を統一することで、1 つのループにまとめられる。
    2. 条件分岐の統一

      • if 文や switch 文などの条件分岐が似ている場合、それらを完全に一致させることで、1 つの分岐に統合できる。
      • 例えば、if (x == 1) {処理 A} と if (x == 1) {処理 B} のようなものがあれば、処理 A と B を統一することで、条件分岐を削減できる。
    3. メソッドの統一

      • 似たようなメソッドが 2 つある場合、それらを完全に一致させることで、1 つに統合できる。
      • 例えば calculateSum() と computeTotal() のようなメソッドがあったとして、どちらも内部で似たような処理をしているなら、それらを統一することで冗長なコードを削減できる。
    4. クラスの統一

      • 似たようなクラスが 2 つある場合、それらを完全に一致させることで、1 つに統合できる。
      • 例えば User クラスと Customer クラスが非常に似ていて、Customer には User にはない数個のプロパティがあるだけなら、User クラスを拡張する形に変更するか、1 つに統合することで、設計がシンプルになる。

変更の分離

  • 一つのメソッドの内部処理を変更する場合、そのメソッドの変更必要箇所と、そうでない部分を切り分け、変更必要箇所のみを変更していく。
  • これにより、変更部分以外への影響をなくす。

データ構造の変更

  • 🔵 「内側から外側へ変更する方法」

    • 先にデータ構造を変更し、後でその利用部分を修正していく
    • 先に厨房を変更し、後からお客様向けメニューを変える。
  • 🔴 「API(外部)から変更する方法」

    • 先に API のリクエスト形式を変更し、後でその内部処理を変える。
    • 先にお客様向けメニューを変え、段階的に厨房の仕組みを移行する。
  • どちらの方法も「最終的にシステムを移行する」ことが目的だが、以下の使い分けをすると、スムーズな移行が可能になる

    • 内部の混乱を防ぎたいなら → 内部を先に変更する(内側 → 外側)
    • 外部システムとの互換性を維持したいなら → 外部を先に変更する(外側 → 内側)

メソッドのインライン化

  • メソッドの制御フローが散らかってしまった場合、一度メソッドをインライン化し、何がどうなっているのかを精里してから再度リファクタリングしてみるのもよい。

メソッドオブジェクト

  • 抽出部分をオブジェクトにする方法
  • 必要な引数やパラメータをインスタンス変数として保持し、処理は run()によって実行される
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?