開発が「快適である」状態を維持するために、テストコードに求める質について述べる。
- テストの表現力を高める必要がある
- テストコード自体がメンテナンスの足かせにならないようにする
- テストがしづらいということは、大抵プロダクションコードの設計が間違っているということを示すヒント
- TDDはテスト/仕様/設計をひとつの全体的な活動としてまとめあげるもの
-
TDD
- テストを使って設計を進めるのがコツ
- TDDはコードの外側(機能、性能)の質を検証するものだが、内側の質に関するフィードバックも得られる
- フィードバック例) クラス間の結合度やまとまり、(保守性維持に効果的な)情報隠蔽、(明示された or 隠れた)依存性
-
テストを書くのが難しいと感じたら、それは大抵(テスト対象の)設計に改善の余地がある
- まず設計見直しを考慮。テスト側で頑張って対処するのはその後の手段。
- 一般に、テストしやすいコード == 変更に強いコード、でもある。
- 「きな臭いテスト」を嗅ぎ分けて設計を良くしていこう!
-
「きな臭いテスト」
- 本章では、これがコードの設計に関して何をほのめかしているかを(良くある例を取り上げて)扱う。
- 二種類ある
- テスト自体の書き方がまずくて不明瞭になったあり壊れやすくなったりしているもの
- テスト対象のコードに問題があることがテストによって浮き彫りになっているもの
- 前者はこの章の対象外
シングルトン:
- コードの複雑さを軽減させる手段の一つ
- 共通で使用可能なオブジェクトをグローバルにアクセスできるようにすることで、引数で持ち回す必要がなくなる
- 便利なテクニックだが、コストにもなる
Dateの例:
- Dateを内部的に利用している Receiver クラスがある
- (Date内部の)現在時刻取得は System.currentTimeMillis() によって行われている
- 「日を跨いだら受信に失敗する」という(Receiverの)機能をどうやってテストする?
- 日が変わる瞬間にテストを実地するようにする?
- バイトコードを弄って、(Dateの)コンストラクタに割り込む?
- => テストが大変!
- 日時を管理する Clockクラス を作成して、そのインスタンスを Receiver に渡すようにすれば解決
- 特殊なことをしなくてもテスト可能になった
- (より重要なのは) Receiverの時刻への依存性が明示されるようになった
Clockオブジェクトを導入したことで分かったことがある
- 日付に関するポリシー(ex. タイムゾーン、ロケール)がReceiverクラスに紛れてしまっている
- Receiverは本来、単に日付が変わったかどうかだけに関心がある
- => 同一日付かどうかの判定処理を Clock に委譲
さらに進めて、
- Receiverは日付変更チェックにしか日付を利用していない
- SameDayCheckerクラスを作って、そこに日付関連の全ての処理を委譲してしまう
- => 日付に関する処理(SameDayChecker) と リクエストに関する処理(Receiver) が分離。それぞれの振舞のユニットテストも容易に。
- グローバル変数は、コード上の明示的な依存性を見えなくする(隠す)ことができるが、依存性は残ったまま。
- テスト時に気づき悩まされることになる
- オブジェクト指向の目的のひとつは「それぞれのオブジェクトの境界を明確に可視化する」ということ
- オブジェクトが扱う値やインスタンスは以下のどちらかだけにすべき
- ローカルな(自分のスコープで生成/管理している)もの
- 明示的に渡されたもの (「コンテキストからの独立」(p.59))
- オブジェクトが扱う値やインスタンスは以下のどちらかだけにすべき
- Receiverの例では、日付検査のテストをしやすくするという行為によって、
- Receiverの要件がより明示的になった
- ドメインについてより明確に考えざるを得なくなった
- クラスローダーやバイトコードを弄って、対象コード自体は変更せずにユニットテストの依存関係を排除できるフレームワークもある
- しかし、このようなテクニックを使ってプロダクションコードを書く開発者はほとんどいない
- 本当に必要な時もあるかもしれないが、このようなツールを使うことで目に見えないコストが発生することは意識する必要がある
フィードバックは大事
- 設計時の依存関係管理のまずさを回避させてしまうようなテストツールは、貴重なフィードバック元を無駄にしている
- 設計上の弱点は放置されたままになり、関連する他の部分や後々の負債となってしまう
サポートログと診断ログは別のフィーチャ (たまたま実装が共有されていることが多いが)
- サポートログ
- error や info
- サポートスタッフやシステム管理者、オペレータ用
- アプリケーションのUIの一部で、障害の調査や稼働中のシステムの運用管理などに使われる
- 診断ログ
- debug や trace
- 開発者用
- 開発中に内部理解用に使用される。運用時には出力されてはいけない
違いを考慮して、二種類のログ出力にはそれぞれ別のテクニックを使うべき:
- サポートログは、監査や障害回復などの誰かの要求に基づいてテスト駆動で作るべき
- メッセージは要求を満たしているか、正しく動くか、既存の解析ツールが動かなくならないか、などをテスト
- 診断ログは、プログラマの必要に応じて作られ、一貫性も要求されないのでテスト駆動である必要はない
ロガーのような静的なグローバルオブジェクトのユニットテストを書くのは難しい
- ファイル読み込み、アペンダオブジェクトの操作、終了時の後始末、ログレベルの調整
- テスト内のノイズを見れば「アプリケーションのドメイン」と「ログ出力のインフラ」の二つのレベルで動作していることが分かる
- プロダクションコードも同様
- 「単一責任原則」に反している
- メッセージ通知(オブジェクト) と その表示方法 を分離すべき
- ユニットテストは前者に対してだけ行えばよく、その際には通常のテスト技法が使用できる
-
サポートログをカプセル化するのはやりすぎ? => 一考の価値はある
- コードの表現力が高まる (実装の詳細にではなく、やりたいことに関するコードを書く)
- レポーティング方式の一貫性を保ったり再利用を促すのも容易
- レポートの構成や制御を、アプリケーションドメインで考えられるようになる
- 個々のレポートに関するテストを書くことで「この例外をどう処理すればいいか分からないから、とりあえずログを吐いて次に行く」病を避けることができる
- この病気にかかるとログはどんどん膨張し、(あいまいなエラー条件を処理していないので)プログラムはうまく動かなくなる
-
(サポートログの)ロガーのテストがし難いとしたら、それは「きな臭いテスト」かもしれない
- ex. 「ドメインオブジェクト全体でログを記録しているので、ロガーをテストに渡すことができない」(???)
- 設計が十分に明確化されていない
- 診断ログが紛れている
- ログ出力が多すぎ (理解が浅い段階で書いたために)
- ドメインコードに重複が残っていて、運用時にログを記録すべき「難所」を発見できていない
-
診断ログ
- 実際にどうように扱うかはシステムによって異なる (使い捨てにするか、テストと保守の対象に含めるか)
- いずれにせよ、サポートログとは別物だと区別しておく必要がある
- その上で診断ログ固有のテクニックを使うのもあり (ex. アスペクト)
- 診断ログをインラインで記述するのは、プロダクションコードを汚染するので、間違ったテクニック
具象クラスのモック
- モックを作りたいクラスを継承して、テストから呼ばれるメソッドをオーバーライドする
- このテクニックを使うのは、他の選択肢がないときに限るべき
CdPlayerオブジェクトの例:
- 問題: CdPlayer と MusicCentre の間の関係を暗黙のものにしてしまう
- MusicCentre が CdPlayer のどの機能(メソッド)に依存しているのかが分からない (明示できない)
- scheduleToStartAt()が最低一回呼び出されていることは分かったけど、その他のメソッドは?
- TDDの狙いは、モックオブジェクトを使ってオブジェクト間の関連を浮き彫りにすること
- モックを使えば MediaCentre が必要なのは、実は CdPlayer ではなく ScheduledDevice ということが分かる
- インターフェースの発見、名前付けによるドメインについての理解の向上
コードがきな臭いことを実感せざるを得ない場面がいくつかある:
- レガシーコードの改善作業中
- サードバーティのコードを扱っている時
- => モックではなくラッパーを作る方がよいが、そうするだけの価値がない場合もある (19章のLogger)
- 何とか妥協する方法を探らねばならない
- コード中にきな臭さを残しておけばおくほど、設計のもろさに泣かされる可能性が高くなる
- クラス内部のフィーチャのオーバーライドは絶対駄目
- したくなったら、それはテストからの警告と捉えるべき
- 値のモックは無意味なので行わない
- もともと不変なのものなので、単にインスタンスを作成して使えば良い
- モック不要な値かどうかを判別する経験則
- 値が不変 かつ
- その値をインターフェースにした場合に、適切な実装クラス名が思いつかない (XxxImplなどはダメ)
- インスタンス作成が大変でモックで代替したいと考えているなら、ビルダーを検討すると良い
- コンストラクタの引数が長くなりすぎることがある
- 依存関係が見つかる度にひとつずつ追加していった結果
- 手に負えなくなったら、外に引き出せる暗黙の構造が隠れていないかを、探して整理しよう
- 例: MessageProcessor
- 六個のコンストラクタ引数 => 全てにエクスペクテーションを書こうとすると大変
- 引き通のうちのいくつかを一つの概念にまとめることができるかも
- counterpartyFinder は unpacker内 に併合
- locationFinder と notofier は、まとめて MessageDispatcher として置き換え
- オブジェクトをまとめたことで 設計が明確に かつ ユニットテストがしやすく なった
- 暗黙のコンポーネントを抽出する際には「クラス内に常に同時に使われている」かつ「生存期間の同じ」引数を探す
- 発見したら適切な名前をつけて切り出す
- 「肥大化したコンストラクタ」のもうひとつの症例
- あまりにも多くの責務を持たせすぎたためにオブジェクト自体が肥大化してしまっている
- テストスイートも同様に混乱しているので「きな臭さ」が分かる
- 分割しても互いに何も共有しない部分があるはずなので、テストもオブジェクトもスライスしてしまう
- 「肥大化したコンストラクタ」の三番目の症例
- 引数の中に依存ピアではないものが存在する
- コンストラクタで渡すのは「依存ピア」だけにすべき
- 「通知ピア」や「調整ピア」は初期値はデフォルトで、後から変更可能なようにしておく
- RacingCarの例
- 依存ピアは track だけ
- ひとつのテストにエクスペクテーションがたくさんありすぎると、何が重要で何をテストしたいのかが分かりにくくなる
- 例
- 意図を明確にするためにスタブとエクスペクテーションとアサーションをきちんと区別すると良い
- スタブ: 実際の挙動をシミュレートしてテストの実行を支援するもの (allowing -> will)
- エクスペクテーションとアサーション: あるオブジェクトが隣接オブジェクトとどのようなやり取りをするのかを確かめるためのもの
テストのきな臭さに気をつけることを覚えると、こんな恩恵を受けられることが分かった。
- 知識をローカルに保つ
- 「裏技」を使う必要性を感じる等で、コンポーネント間で知識が漏れだしていることを発見できる
- ローカルに保てば、コンテキストへの依存性がなくなり、コンポーネントの移動や変更が容易・安全になる。
- 明示的にすれば、名前をつけることができる
- 具象クラスのモックは、オブジェクト間の関連に名前をつけづらいから嫌
- 名前をつければつけるほど、ドメイン情報が増える
- オブジェクト同士に関係に着目することで、実装レベルではなくドメインの観点からオブジェクトの型やロールを定義できるようになる
- 小さな抽象化を数多く行うことで、ドメインのボキャブラリから成るDSLが(元のプログラミング言語の上に)構築される
- データではなく振る舞いを渡す
- 「命じよ、訊ねるな」の徹底は、振る舞いを(コールバック形式で)システムに渡すようなコーディングスタイルにつながる
- 17章のSniperCollectorの例
- インターフェイスが的確であるほど、よりよい情報隠蔽と明確な抽象化ができるようになる
筆者たちは、作業しながらテストやコードそのものを常にクリーンに保つことを心がける。
- 対象ドメインについて理解しやすくなる
- 新しい要件にからむ設計変更に対処できなくなるというリスクを軽減できる
- クリーンに保つ方が、腐ったコードを復元するよりもずっと簡単
ユニットテストが1000行にもなるなんて!
- 質の低いテストがあると開発速度は徐々に遅くなる
- テスト対象のシステムの内側の質がまずければテスト自体の質も低くなる
- 内側の質に関するフィードバックに気をつけることで未然に防ぐことができる
読みやすく柔軟なテストを書くように心がけていると、TDDは開発を邪魔することなく、開発を助けてくれるようになる。