いい話。だいたい同意見で、テストはなるべく書こうとしたい。後からコードに変更を加える人が安心できるように、テストには書いてるコードがどう有るべきかという情報が全部網羅されていてほしい。コードがあるべき状態ではなくなって動かなくなったときは、必ずテストが落ちて欲しい。
とはいえ、テスト書きすぎてしまって良くなかったなあと思うことはある。アプリケーションの設計の階層が無駄に深くなっていて、各階層ごとに似たようなテストをなんども書く事態に陥ったりするような場合だ。
例えば、何かブログみたいなWebアプリを作っていて、エントリー投稿する機能を実現する機能が以下のクラスに含まれていたとする。
- Blog::Controller::Entry
- ディスパッチャから呼ばれるエントリーポイント
- Blog::Handler::Entry
- HTTPリクエストからEntryの情報を読み込んでくれるオブジェクト
- Blog::Service::Entry
- 実際にエントリーを投稿する機能をもつクラス
Ruby風の擬似コードで書くとこういう感じで使われるイメージ。
class Blog::Controller::Entry # ... def post(req) handler = Blog::Handler::Entry.new(req) handler.post end # ... end class Blog::Handler::Entry # ... def post Blog::Service::Entry.post(self.title, self.content) end # ... end class Blog::Service::Entry # ... def self.post(title, content) # 実際に投稿する end # ... end
1つの機能が3つの階層の組み合わせで実現されている。いろいろ無駄っぽいけど、フレームワークにあわせないといけないなどの、設計の事情でこうなっているとする。
どれもエントリーを作る機能を含んでいるが、それぞれの階層で要求される引数も違えば、返却値も違うのでテストは別に書く必要がある。機能は似ているので、必要なテストケースも似通ってくる。例えば、エントリーの本文が空であった場合にどう振る舞うか、といったテストケースがそれぞれ必要である。どこに注目するかという点は階層ごとに違うが、1つ機能を追加してテストしたいケースが5つあるなら、3階層あるので、15個のテストを書かないといけないというふうになる。
構造化が進み、モジュールの数が増えて、モジュール間の通信が増えれば、自ずとそのインターフェースのテストも複雑になる。エントリーを投稿する機能を作り始めたところであれば、 Blog::Controller::Entry#postに直接エントリー投稿機能を実装すれば構造はシンプルになる。コードの重複が発生し、コードの見通しがわるくなったら、構造化をすすめるようにしたい。少なくとも頻繁に変更される部分に階層を導入する場合は、その複雑さが波及する範囲にテストも含めて考えたい。
機能追加による変更が局所的になるように、よく構造化されながらも、理解しやすくテストしやすいスッキリした構造になっているみたいなのを、維持していくのがむずい。
— hakobe (@hakobe) 2013, 10月 14