はじめに
テストのないコードは悪いコード(レガシーコード)である。
これがマイケル・C・フェザーズの強いメッセージです。 どれだけうまく書かれているかは関係ない。テストが書かれていない=悪であるとまで言い切っています。 どれだけ美しいか、オブジェクト指向か、きちんとカプセル化されているかは関係ない。テストがなきゃだめだ!
第1部 変更のメカニズム
第1章 ソフトウェアの変更
- リファクタリングでは、
- 小さな変更を繰り返し行う。
- 変更を容易に行うためテストでサポートする。
- 機能を変更すべきではない。
- コードの変更時に問題になるのは、影響範囲を把握できないこと。変更を安全に行うために最も重要なことは、影響範囲を理解すること。
- 変更を避けると腕がなまってしまう。大きなクラスを分割する作業は週に2,3回くらい行っていないと、困難な仕事になってしまう。頻繁に変更を行っていれば、何が分割できるかの検討をつけやすくなり、変更作業が容易になる。
第2章 フィードバックを得ながらの作業
- テストを用意することには、「正しいことを確認するため」だけでなく、「変更を検出するため」という目的もある。
- 特定のコードにエラーがあると思ったときにテストハーネスを利用できれば、手早くテストコードを書いて、本当にそこにエラーがあるのか確認することができる。
- 優れた単体テストの条件
- 実行が速い: 速く走らないとしたら、それは単体テストではない。0.1 秒もかかっていたら遅い単体テストである。「DBアクセス」、「ネットワーク通信」、「ファイルシステムアクセス」、「実行するための環境設定」が必要なものは単体テストとは言えない。
- 問題個所の特定がしやすい
- 他のクラスと依存関係があるクラスはテストハーネスに入れられない。例えば、
DbConnection
に依存しているクラスをテストしたい場合は、まずはインタフェースIDbConnection
を導入して依存を切る必要がある。 - 価値をもたらす機能的な変更を行いながら、システムのより多くの部分をテストで保護していくべき。
- 扱いやすくなるように大きなクラスを分割するといった些細なことによって、アプリケーションに大きな違いが出てくる。
第3章 検出と分離
- テストを整備する際に依存関係を排除するのには2つの理由がある。
- 検出: コードの計算した値にアクセスできないときに、それを検出するために依存関係を排除する。
- 分離: コードをテストハーネスに入れて実行することすらできないとき、分離するために依存関係を排除する。
- ソフトウェアを分離するには様々な方法があるが、検出のための主な手段は「強調クラスの擬装」のひとつしかない。
- あるクラスの依存をインタフェースの導入によってなくした場合、そのクラスに FakeObject を渡してテストすることになる。FakeObject ではテスト対象のクラスから渡されたデータを記録することで、テスト対象のクラスが正しくデータを渡しているかを検証できる。依存部分を切り離すことにより、依存していたインタフェースでやりとりしているデータが正しいかどうかをテストすることができる。
- オブジェクト指向以外の言語では、代替関数を定義して、その中でテストからアクセスできるグローバルなデータに値を記録する。
- 擬装オブジェクトに、結果が正しいかを確認する機能を追加したものが「モックオブジェクト」。モックオブジェクトは協力なツールだが、すべての言語に対応するモックオブジェクトフレームが提供されているわけではない。しかし、大抵の場合は、単純な擬装オブジェクトで十分である。
第4章 接合モデル
- テストが可能になるようにクラスを取り出す作業を行っていると、どのような設計が優れているかという基準が変わってくる。
- ソフトウェアを接合部 (Seam) という観点から見ることで、コードにすでに含まれている依存関係を排除するための手掛かりを見出すことが可能になる。Seam とは直接その場所を変更しなくても、プログラムの振る舞いを変えられることのできる場所。
- Java では同じ名前のクラスを別のディレクトリに置き、CLASSPATH を変更することで別のクラスにリンクすることができる。(リンク接合部: link seam)
第5章 ツール
- 自動リファクタリングツール: リファクタリングツールを使って自動リファクタリングを行う場合、振る舞いが変わっていないか注意する必要がある。例えば、リファクタリング後に、あるメソッドの呼び出し回数が変わっている場合、そのメソッドに副作用があると振る舞いが変化する。
- モックオブジェクト (mock object): 他のコードを取り除いて、テスト時に自分のコードを完全に実行させるには、代わりに正しい値を返してくれるものが必要になる。オブジェクト指向のコードでは、これをモックオブジェクトと呼ぶ。
- 単体テストハーネス: xUnit テスティングフレームワークは、ほとんどの言語に移植されている。JUnit, CppUnit, NUnit…
- 一般的なテストハーネス: FIT (Framework for Integrated Test) は統合テスト用のテスティングフレームワーク。システムに関する文書を書き、その中にシステムの入力と出力について記述した表を含めることができれば、FIT フレームワークがテストを実行してくれる。FitNesse は wiki 上に構築された FIT で、FIT テストを定義するときに階層構造の Web ページを使うことができる。
第2部 ソフトウェアの変更
第6章 時間がないのに変更しなければなりません
- 一般的に変更箇所は集中する。今日ソフトウェアを変更しているのであれば、近い将来、そのすぐ近くを変更することになる。
- 今すぐクラスを変更しなければならないのなら、テストハーネス内でそのクラスのインスタンス化を試みてみる。
- システムに新しい要件を加えるときの方法。
- スプラウトメソッド (sprout method): 新しい機能を新しいメソッドとして記述する方法。既存のコードに直接手を加えるよりも望ましい結果をもたらす。
- 長所: 古いコードと新しいコードを明確に区別できる。
- 短所: 元のコードはテストするわけでもなく、改善するわけでもない。
- スプラウトクラス (sprout class): 変更部分を新しいクラスで実現する方法。変更対象の既存のクラスをテストハーネス内でインスタンス化できない場合は、テストを書けないのでスプラウトメソッドは使えない。既存クラスの依存を排除するのに時間がかかりすぎる場合、新しいクラスを導入して、そこだけテスト可能にするという方法がある。
- 長所: コードを直接書き換える方法よりも、確信を持って変更を進められる。
- 短所: 仕組みが複雑になること。
- スプラウトメソッド (sprout method): 新しい機能を新しいメソッドとして記述する方法。既存のコードに直接手を加えるよりも望ましい結果をもたらす。
第7章 いつまで経っても変更作業が終わりません
あまりに多くの依存関係を持つクラスは、大きなコードの塊を切り離し、テストで保護できるかどうかを調べるとよい。
第8章 どうやって機能を追加すればよいのでしょうか?
この章には、TDD の基本的な進め方が述べられています。
- 強力なリファクタリングは数多くあるが、最も強力なのはクラス名の変更である。開発者のコードの見方を変え、考えもしなかった可能性に気づかせてくれる。
第9章 このクラスをテストハーネスに入れることができません
- コンストラクタで渡しているオブジェクトが外部リソースなどを利用している場合、そのクラスのインタフェースを抽出し、擬装オブジェクトを渡してテストするようにする。例えば、コンストラクタでデータベースへの接続を行うオブジェクトを渡している場合は、その FakeConnection などの擬装オブジェクトを渡すようにする。リファクタリングをサポートしたツールがあれば、インタフェースの抽出は簡単にできる。
- テストコードはきれいであるべき。それは簡単に理解でき、変更できるものでなければならない。
- テストしたいクラスが、生成しづらいパラメータを必要としていたら、Null を渡すことを考えてみる。実際にそのパラメータがテスト中で参照されたら例外を投げるので分かるはず。実際にそのパラメータが必要になった時点で、必要なオブジェクトを生成するように変更すればよい。ただし、C++ のように Null ポインタエラーを検知できない言語ではこの方法は使用できない。
- 通常は Null を本番コードで使用すべきではない。Null オブジェクトパターンなどを適用できないか考えてみる。
- あるクラスをテストする際に、依存するクラスのインスタンスを作らなければならない時、そのクラスの振る舞いがテストに影響することがある。例えば、その依存クラスのコンストラクタで DB へのコネクションを張っていたりすると単体テストが書けない。このような場合は、依存クラスのサブクラスを作って connect() などをオーバーライドしてしまえばよい。これを可能にするためにも、コンストラクタでのハードコードは避けてメンバメソッドを呼び出すような実装を心がけるべき。
- 依存クラスのコンストラクタ内で、外部リソースやライブラリに依存するクラスのオブジェクトを生成している場合もテストができなくなる。その場合は、コンストラクタのパラメータでオブジェクトを渡すようにする(コンストラクタのパラメータ化)。元々のシグネチャを持つコンストラクタは残しておいて、新しくパラメータをとるコンストラクタを追加すればよい。コンストラクタ内でオブジェクトが生成されていて、生成の依存関係がない場合には、コンストラクタのパラメータ化が非常に簡単に適用できる。
- テストハーネスの中で Singleton を含むコードを実行するには、Singleton の制約を緩和する必要がある。
- Singleton クラスに、Singleton インスタンスの setter メソッドを用意する。
- Singleton クラスのコンストラクタを private から protected に変更する。
- Singleton クラスのインタフェースを抽出し、テストコードから擬装オブジェクトを生成して Singleton クラスにセットする。あるいは、Singleton クラスをサブクラス化して、各メソッドをオーバーライドする方法もある。
第10章 このメソッドをテストハーネスで動かすことができません
オリジナルのコードは複雑すぎるので、ここでは簡素化したコードに置き換えてざっと説明してみます。
テストしたいメソッドが private である
- private メソッドをテストしたい場合、そのメソッドは public にすべきである。
- public メソッドにすべきかどうかで悩んでしまう場合、大抵は、そのクラスが多くのことを行いすぎであり、修正すべきことを意味している(1つのクラスが複数の責務を持ってしまっている)。
- よい設計はテスト可能であり、テスト可能でない設計は悪い設計である。
▲まく注記: 例えば、複雑なアルゴリズムや演算処理を担う private メソッドがあるのであれば、それはおそらく専用クラスの public メソッドとして作成し、テストを記述すべきということでしょう。
private メソッドを別クラスに括りだして、public 化する余裕がない場合は、以下のようにテスト用のラッパークラスを作成して解決する方法がある。
- この方法は、メソッドを単純に public 化するのと本質的には変わらないので微妙な対応方法だが、リファクタリングすべき箇所の目印となる。
- Java などの言語ではリフレクションによって private メソッドのテストを記述することはできるが、根本的な依存関係の問題を先延ばしにしているだけである。その種のごまかしをすると、コードがどの程度悪くなっているのかに気付きにくくなってしまう。
標準ライブラリの継承できないクラス、インスタンス化できないクラス依存している
Java の final クラス、.NET の sealed クラスのような、継承できないクラスに依存したコードがあると、うまくテストがかけないことがある(インスタンス化できなかったり)。 このような制御不可能なライブラリのクラスに直接依存したコードがあったら、将来の変更に対応できるようにインタフェースを抽出し、ラッパーで隔離するのがよい。
例えば、.NET の sealed なクラス HttpPostedFile
に直接依存したコードがあったら、IHttpPostedFile
インタフェースとして抽出し、HttpPostedFileWrapper
というラッパクラスを作成する。
元の HttpPostedFile
クラスに依存したコードを、IHttpPostedFile
インタフェースを使用したコードに置き換え、HttpPostedFileWrapper
経由で HttpPostedFile
クラスを使用するようにする。テストコードでは、IHttpPostedFile
を実装したスタブクラスを用意すればよい。
GUI に強く結びついたコードになっていて結果を検出できない
例えば、下記の UI 系クラスは、イベントハンドラ内で表示内容の構築から UI 表示までの処理を一括で行っている。 このイベントハンドラを実行した結果は、実際の UI 上の表示が更新されるという形で表れるので、テストコードから直接呼び出しても結果の妥当性を判断することができない。 また、表示内容を構築するロジックが UI 系のクラスに依存していると、その部分を別クラスに抽出することもできなかったりする。
このようなケースでは、UI 系クラスをサブクラス化し、取得したい結果だけをフックして参照できるようにしつつ、実行されてはいけない UI 処理をオーバーライドにより削除してしまうという方法がある。 一言で言えば、「サブクラス化してテストしたい部分だけを生かす」ということ。
そして、テストコードでは、この UI 系のクラスをサブクラス化し、処理結果だけを機械的に取得できるようにする。
UI 制御を行っている部分は updateView
メソッドとして分離してあるので、オーバーライドして空っぽにしてしまえば、テストコードを実行したときに UI が表示されてしまうのを防ぐことができる。
▲まく注記: もちろん Frame クラスが単独でインスタンス化できないと、このようなテストは不可能ですね。 ここでは、どうしても部分的にしかテストできないクラスがあるときに、何とかしてその部分だけでもテストできるようにする方法の一例があげられているのだと捉えればよいでしょう。
コラム: コマンドとクエリーの分離
コマンドとクエリーの分離 (Command/Query Separation) は、Bertrand Meyer(Eiffel の開発者)が設計の原則で、メソッドはコマンドあるいはクエリーのいずれかであり、両方にすべきではないというもの。
- コマンド: オブジェクトの状態を変更できるが値は返さないメソッド。
- クエリー: 値は返すがオブジェクトの状態を変更しないメソッド。
この原則に従うことでわかりやすい設計になる。 例えば、メソッドがクエリーであれば、副作用を起こさずに何度も続けて呼び出せることがすぐにわかる(C++ ではメソッドに const キーワードが付いていれば、クエリーであることがすぐにわかる)。
第11章 変更する必要がありますが、どのメソッドをテストすればよいのでしょうか?
テストすべき場所はどこか
この章には、変更の影響範囲を調べ、テストする部分を決めるまでの流れに関して記述されています。 基本的な手順は次の通り。
- 変更の影響範囲を調べて、どこで変更を検出することができるかを明らかにする。影響スケッチ (effect sketch) を描くとよい。
- どこで影響を検出できるかを明らかにしたら、その中から選んでテストを記述する(影響が伝搬していった先の方でテストを書くと効果が大きい)。
影響は基本的に以下の3種類の方法で伝搬する。
- 戻り値が呼び出し側で使われる
- パラメータとして渡されたオブジェクトが変更される
- 静的データや、グローバルなデータが変更される (書籍には明記されてませんが、メンバ変数などの変更もこれに含むと考えればよい)
メソッドの利用者になり得るスーパークラスとサブクラスも忘れずに確認する必要がある。
あと、使用している言語についてよく知っていることは重要。
例えば、C++ では、変数宣言に mutable
キーワードを指定した場合、const
メソッドからその変数を更新することができてしまう。
const
メソッドだから、意味的に const(変更されない)と捉えていると危険なことになります。
影響を単純化する
小さな重複部分を削除することが、影響の広がりを小さくすることに繋がる。
例えば、下記の影響スケッチは、declarations
というフィールドの変更が、getInstance
メソッドと getDeclaration
メソッドに影響することを示している(言い換えると、2つのメソッドが declarations
フィールドを参照している)。
getInterface
の実装の中で直接 declarations
フィールドを参照せず、getDeclaration
メソッドを呼び出すように変更すれば、影響スケッチは下記のようにシンプルになる。
こうすることで、getInterface のテストをすれば、getDeclaration のテストもできたことになる。 影響スケッチが末広がりな形になっていたら、このような小さな変更で改善できないかを考えてみるとよい。
著者の意見
著者の意見として、「影響を調査できる統合開発環境があるといいなぁ。コードの一部を選択してホットキーを押すと、選択したコードの変更によって影響を受ける、すべての変数とメソッドを示してくれるようなもの」というものがあります。
▲まく注記: 確かにこのようなものが一般的になれば、ユニットテストや、リファクタリングもずいぶん作りやすくなりそうです。 ただ、フレームワークによっては影響範囲というものは動的にしか決まらない部分(Android の Intent のように、レシーバーが動的に決まるものなど)も多々あるので、なかなか難しいのかもしれません。
マイケル・C・フェザーズは、カプセル化とテストが対立した場合(どちらかを諦めないといけない場合)、テストによる保護を優先する立場を取っています。 カプセル化自体は目的ではなく、理解するための手段であるということ、テストはコード調査を簡単にし、将来カプセル化を強めるために役立つということなどを理由に挙げています。
第12章 1カ所にたくさんの変更が必要ですが、関係するすべてのクラスの依存関係を排除すべきでしょうか?
割り込み点
割り込み点 (interception point) とは、特定の変更による影響を検出できるプログラム上の場所のこと。 例えば、private フィールドに影響が及ぶとしても、そこでは影響を検出できないので、割り込み点ではない。 多くの場合、変更のための最善の割り込み点は、変更しようとしているクラスの public メソッドになる。 変更点のすぐそばにある割り込み点を選択するのが良い考えである。
絞り込み点
絞り込み点 (pinch point) とは、影響スケッチの集約された場所であり、少数のメソッドに対するテストで、多くのメソッドの変更を検出できる場所である。
つまり、割り込み点の中で、テストを書くべき第一候補となる場所である。
例えば、ある変更に関して下記のような影響スケッチが描けた場合、絞り込み点は BillingStatement.makeStatement
となる。
ここにテストがあれば、そこより上の影響をすべて把握することができる。
ただし、多くの場合、絞り込み点を見つけるのはほとんど不可能である。 直接的に影響を及ぼすものがたくさんあると、影響スケッチは大きな絡み合った木のようになってしまう。 そのような場合は、一度にあまりにたくさんの変更をしようとしているのかもしれないので、1つや2つの変更だけを取り上げて絞り込み点を探すのがよい。 最終的に絞り込み点が2つだけ見つかったときに、そのどちらか片方だけにテストを書くだけで十分かどうかを判断するには、「そのメソッドを壊したら、この場所で変更を検出できるか?」と問いかけてみればよい。
既存のコードに変更を加えるとき、絞り込み点を見つけて、そこにテストを書くのがファーストステップになる。 絞り込み点のテストは、森の中に歩いて立ち入り、線を引いて、「この区域すべては私のものだ」と言うようなものだ。 その後、その区域内のリファクタリングを行い、より細かい粒度でテストを書いていけばよい。 最終的には絞り込み点のテストは削除し、それぞれのクラス用の単体テストを使って開発を進めることができるようになる。
絞り込み点を見つけることは、コード改善のためのヒントにもなる。 絞り込み点は、自然なカプセル化の境界を示している。 大きすぎるクラスがある場合、影響スケッチを描いて絞り込み点を見つけることで、どの境界でクラス分離するのがよいかが分かる。
第13章 変更する必要がありますが、どんなテストを書けばよいのかわかりません
ほとんどのレガシーコードでは、すべてのバグを見つけて修正することを目標にすると作業は決して終わらなくなってしまう。 レガシーコードを修正する必要があるときは、ソフトウェアの仕様書やプロジェクトのメモを掘り起こしてテストコードを書く方法の他に、仕様化テスト (characterization test) を書いてコードの振る舞いを明らかにする方法がある。
仕様化テスト (characterization test)
仕様化テスト(マイケル・C・フェザーズの造語)は、コードの実際の振る舞いを明らかにするテスト。 「システムはこれをすべきだ」とか「こうしていると思う」ということを確認するテストではない。 仕様化テストは、システムの現在の振る舞いをそのまま文書化する。 下記が仕様化テストを書くときの手順である。
- テストハーネスの中で対象のコードを呼び出す
- 失敗するとわかっている表明 (assert) を書く
- 失敗した結果から実際の振る舞いを確認する
- コードが実現する振る舞いを期待するように、テストを変更する
- 以上の手順を繰り返す
▲まく注記: ようするに、現在の振る舞いを正しいものとしてユニットテストを書くだけですね。
仕様化テストはどこまで書けばよいか?
仕様化テストは無限に書けてしまうが、いつやめればよいのか? 重要なことは、ブラックボックステストを書いているわけではないということ。 仕様化テストを書くときは、対象となるコードを読むことができる。 コードに加えたい変更について考え、変更に起因するあらゆる問題を、今持っているテストで検出できるかを考えてみるとよい。
できない場合、検出できると確信を持てるまでテストを追加する。 それでも確信できない場合は、別の方法でソフトウェアを変更すること(つまり、仕様書などの文書を調べてテストを記述すること)を考えた方が安全。
仕様化テストでバグを発見した場合
- a) リリース前なら … 修正する。
- b) リリース後なら … 関係者に周知して相談。もちろん修正するのが望ましいが、影響度合いによる。
▲まく注記: 仕様化テストを書いたときは、それが望ましい振る舞いを調べるためのテストではなく、あくまで現在の振る舞いを調べる仕様化テストとして書いたことが分かるようにしておいた方がよいですね。
第14章 ライブラリへの依存で身動きが取れません
特定のライブラリに強く依存したコード → ベンダーがライブラリを値上げする → 利益出ないので乗り換えたい → 無理。死亡。
- ライブラリの直接呼出しをコード内に分散させてはならない。使用するライブラリの変更は絶対ないと考えるかもしれないが、勝手な予想にすぎない。
- 言語の機能を利用して、設計上の制約に従わせようとするライブラリの設計者は、過ちを犯していることが多い。優れたコードはテスト環境でも問題なく動くものである。本番環境に特化した制約は、テスト環境で行いたい作業を不可能にしてしまうことがある。
▲まく注記: 要するに、ライブラリとの結合部はゆる~く置き換えられるような設計になっていないとテストできなくなっちゃうよ。本番環境で完璧に動けばOKという考え方は間違っているよ。ということ。
第15章 私のアプリケーションは API 呼び出しだらけです
ライブラリの API 呼び出しをあちこちで行っているシステムは、自分たちの手作りのシステムに比べて扱いが難しくなる。
- 理由1: 表面的な API 呼び出ししかないので、構造を改善する方法を調べるのが難しい。
- 理由2: その API が自分たちの所有物でないので、インタフェースを改善しにくい。
こういったシステムにアプローチする方法は2つある。
- API をラップする
- 責務をもとに抽出する
API をラップする方法
以下のような状況では API をラップしてしまうのが有効。
- API が比較的小さい。
- サードパーティライブラリへの依存を完全に分離したい。
- API を通じたテストが不可能なため、テストを書くことできない。
責務をもとに抽出する
以下のような状況では、コード内の処理を責務をもとに抽出し、より上位のメソッドとしてまとめてしまうのが有効。
- API が複雑である。
- 安全なメソッド抽出をサポートするツールがあるか、手動での抽出を安全に行う自信がある。
API をラップする方法の方がシンプルに感じるが、実際にはうまくいかないケースがある。
例えば、下記のようなコードで Trasport
のラップすることを考えてみる(テスト用のスタブとしてサブクラスを作る)。
よく見ると、Transport
は Session
から作成されているので、Session
の方をラップしなければいけないことがわかる。
でも、Session
クラスはライブラリ内で final
定義されていてサブクラス化できない。
こうなると、ラッパ用のサブクラスを作成することは不可能なので、2つ目の、責務をもとに抽出するという方法を取らざるを得なくなる。
API をラップする方法の方が作業量は多くなるが、サードパーティライブラリから自分たちのコードを切り離したいときには便利である。 責務をもとに抽出する方法では、コードをより上位のインタフェースに依存させることが可能となるが、抽出したコードはテストで保護できないかもしれない。
第16章 変更できるほど十分に私はコードを理解していません
- コードを印刷することで、印をつけることができるようになる。 長いコードを色分けすることで、責務の分担や、構造の理解に役立てる。
- テストを書かずにリファクタリングすること (試行リファクタリング: scratch refactoring) は、そのコードを理解するために非常に役に立つ。ただし、そのコードはチェックインせずに破棄すること。
第17章 私のアプリケーションには構造がありません
大規模なシステムの全体像を理解する方法のカタログが『Object-Oriented Reengineering Patterns』 に載っている。 他には下記のような方法もある。
システムのストーリーを話す
2人以上でシステムの振る舞いをストーリー仕立てで話すことで、他のメンバに説明する方法。 「このシステムのアーキテクチャはどうなっていますか?」という質問から始め、「他には何かありますか?」と繋げていく。 シンプルな会話ベースで説明することにより、システム自体をシンプルにするのを促す効果がある。 ストーリーは指針を提供する。
白紙のCRC
CRC は、クラス (Class)、責務 (Responsibility)、協調 (Collaboration) の略。 それぞれのカードにクラス名、クラスの責務、強調するクラス(やりとりする他のクラス)の一覧を記述する。 ここで提案する方法は、白紙の CRC カード、つまり、単なる白いカードを使ってアーキテクチャを説明する方法で、カードを何らかのインスタンスに見立ててシステムの動きを表現する。 例えば、あるインスタンスが入力と出力の接続を持つのであれば、1つのカードの上に二枚のカードを重ねる。 コレクションを表現する場合も、カードの上にカードを重ねる。 そして、それぞれのカードを動かしながら、システム全体の動きを説明する。