Michael Feathers氏の最新の投稿をめぐり、ブログのコミュニティ上で活発な議論が行われた(source)。Feathers氏は、オブジェクト指向プログラミング言語に組み込まれた機能を使うと、テストが容易に行うことができ、コードの復元性を簡単に高めることができる(source)と主張した 。
Feathers氏は一つのサンプルを提示した。このクラスXはbadMethodという名前のメソッドを持っている。このメソッドはテストをしにくくするような何か、例えばデータベースを作成/更新したり、低レベルのハードウェアを操作したりする。
public class X {
public void method() {
...
badMethod();
...
}
...
}
クラスごとのテストや、(関連する機能ごとの)クラス群ごとに独立してテストができるような設計が理想である。このケースは理想的とは言えないが、 badMethodがfinalではなく、オーバーライドすることができるメソッドであるため、テストには好都合と言える。この条件であれば、オーバーライドを行ってテストを簡単にするような割り込みをかけることができるため、テストを行うには十分な柔軟性を持っているといえる。
public class TestableX extends X { void badMethod() { // 何もしない } }
これはFeathers氏が継ぎ目(seam)と呼ぶもので、他の箇所を編集することなく、機能を実現する部品を取り替えることができる。これはオブジェクト指向言語が提供する、遅延結合と呼ぶテクニックである。この機能のおかげで、関数型言語よりもオブジェクト指向言語の方が復元性の高いテストが行いやすくなるとFeathers氏は信じている。
ほとんどのオブジェクト指向言語が、プリプロセッサ、継承、ポリモーフィズム、委譲、マクロ、関数ポインタ、高次関数(関数を引数に取る関数)、動的メソッド、ファーストクラス関数(オブジェクトとして引数に渡したり、返り値で返せる関数)、モジュール境界、モナドなどの何らかの機能が、この継ぎ目として使用できる形で提供されているということを、コメントを付けた人や、Feathers氏自身が述べている。議論に参加している人によると、テスト可能性は言語の選択よりも、プログラムの設計による影響の方が大きいという。例えばJohn氏は、どんな言語を使用したとしていても「ユニットテストがしやすいようにコードを構造化するのが先決」と表明している。他のブロガーのAndrew氏は、もしテストされるべきコードの固まりがメソッドとして分割されていない場合には、テストのしやすさを向上させるために実装を変更すべきだと書いている。そのために、彼は「継ぎ目について考えることは、テスト可能性を向上させるためのプログラム設計というのはどういうものか、という問題を取り扱うことと同一である」としている。
これらの反応を受けて、Feathers氏は、これらの機能はほとんどの言語に実装されているが、重要なのは、これらの機能が簡単につかえるかどうかだ、と述べている。特に、コードがテストしやすいように設計されていない場合においては、これが顕著に表れる。
私は、"テスト容易性を高めるための設計"が本当の議論の中心であることを認めます。しかし、残念なことに、どんなにそのように設計したとしても、そうならないシステムがあるということも知っています。そのため、私が注意を払うのは復元性というものです。
[…]
私は継ぎ目をうまく設計することができることは知っていますが、これは問題となるポイントではありません。私が問題だと考えているのは、テスト容易性を高める設計をされていないコードに対して、どうすればこのような仕組みを簡単に組み込めるか、ということです。
[…]
継ぎ目を使ったとしても、いつも思うようにテストが書けるようになるとは限らないというのは事実です。しかし言語のサポートがすばらしければ、偶然のように継ぎ目ができたり、故意に継ぎ目を作るのも容易になるはずです。
Feathers氏のよると、関数型言語にはリンクできる代替モジュールがいくつもある。しかし、「Haskelには例外的に、テスト時には避けたいコードがあり、これらはモナドの内部に集中している」と述べている。
Feathres は「関数純粋性のために、ユニットテストを行おうとする意志がそがれる」と主張したが、この意見に対しては厳しく指摘するコメントが数多く寄せられた。「Feathres氏は関数型言語の特徴と、それによってもたらされる可能性を考慮に入れていない」というものである。Erikd氏は、Feathers 氏がJavaの構造とイディオムを関数型言語のコードに持ち込もうとしているのではないか、と述べている。
最初に彼はOcamlの文法を使ってJavaのコードを書こうとして、しまいにはOcamlはJavaよりも劣っていると不満を述べていました。彼の結論は驚くようなものではありません。Ocamlはシンプルに設計されていて、Javaのようなオブジェクト指向のプログラムを書くには不十分です。
次の問題は、彼が、関数型言語はJavaよりもテストを書くのが難しいと結論づけたことです。Ocaml上でJavaのようなコードを書こうとしていたのであればこの結論は正しいでしょう。しかし、一般的なOcamlのコードや、関数型言語のコードを書く限りでは、この結論はただしくありません。
多くの関数型言語の擁護者は、関数型プログラミングでは副作用が分離されると主張している。Greg M.氏によると、この分離を行うことでリファクタリングが必要となるような悪いコードが書かれることはなくなり、テストはしやすくなる、と説明している。:
関数型言語を使用すると、(テストでは)扱いにくいコードはすべてトップレベル(から最下層の部分まですべて)で分離することができ、ロジックをきれいに保つことができます。
[…]
もし(モジュールの)単体の独立が保証されているのであれば、ユニットテストはとても簡単になります。問題が依存関係にあることは明白です。
Robert Goldman氏もまた、「伝統的なオブジェクト指向プログラミングに従って状態をたくさん使用することは、テストにとって有害でしかない」と述べている。というのも、このオブジェクト指向的なアプローチでは、「テストの準備として、多くのオブジェクトが内部で接続しているような構造を準備する必要があり、期待される副作用を評価しようとすればさらに複雑さが加わっていく」と述べている。一方で「Haskellのような純粋な関数型のフレームワークでは、(テストをしにくくする)原因はモナドの中にカプセル化される」と説明している。Greg氏が述べたように、モナドを使用することで「簡単なコードですぐにIOコマンドをリスト/ストリームに変換するすることができる。他の(IOに依存していた)コードはこのストリームを扱って処理をするような構造になる(ため、IOとの直接の結合をなくすことができる)」。
Ericd氏も同様に、内部状態を持たないという関数型言語の特徴に触れて、状態遷移を扱う必要がない、と述べている。「状態変化のないモジュールやシステム」をテストする場合にも、Feathers氏が書いているような種類のテストを書く必要がなくなる、としている。
(状態遷移がないコードのテストが終わった後は)入力に関するテストを残すのみです。このテストではすべての境界条件をテストできる入力を集めておき、それぞれのケースで関数にデータを入力してテストを通し、出力を検証するだけです。
[...]
もし、コンポーネント(純粋な関数)群が個別にテストできて、正しいと証明できたとすると、これらの純粋関数を合成したものも正しいと言うことができます。
このような反応に対して、Feathers氏は「関数的な純粋さ(という概念)と、関数的によく設計されたコードがこの手の問題を持たないことには気づいていた」と答えている。しかし、すべてのコードがこのようにうまく設計されているわけではなく、「Haskellは副作用をやむを得ず分離した関数型言語の一つでしかない」と述べている。他の言語、例えばOcamlやScalaについては「コードが乱雑になるのを防ぐしかけは何もないように見える」と付け加えた。
そのように言ったとしても、Feathers氏に反対する人の多くは、関数型のコード乱雑になる原因は、関数型ではないイディオムを関数型言語に持ち込んだこと以外にはないと信じている。Goldman氏は「ML, Ocaml、Common Lispといった、ハイブリッドな言語では副作用をもたらすような非関数型のコードを書くことを認めているが、他の言語では許可されていない」、と述べている。Greg氏はこの流れにのり、関数型言語に対立するような非関数型の書き方をするようなことをやめれば、責務ベースのオブジェクト指向のコードでプログラマを悩ませる、制御の反転(Inversion of Control: IoC)や、感心の分離(separation-of-concern)の悩みとは無縁となる、と唱えている。Erikd氏はこの意見を論拠として、オブジェクト指向の世界から入った人が質の高いコードを関数型言語で書くための方法として以下のように述べている。「古い習慣や考え方を捨て、可能な限り、オブジェクト指向の機能や、責務ベースのプログラミングの機能を無視する『こと』が必要である。」
原文はこちらです: http://www.infoq.com/news/2008/03/revoerability-and-testing-oo-fp