TDD再考 (8) – 凝集性(cohesion)とは何なのか?

Is TDD Dead? 会談

David Heinemeier Hansson氏の「TDD is dead」は、アジャイル界隈に醸成されていたある種の信仰を、過激な論調で批判したことによって大変なセンセーションを巻き起こした。強い言葉に対しては、やはり感情的な反応が多くなる。中には坊主憎けりゃ袈裟まで憎いという感じで、TDDを超えてRailsの設計思想を批判する人まで現れたが、「TDD is dead」を受けて行われた「Is TDD Dead?」という歴史的な会談の内容を見るに、DHH氏の発言は、規律(信仰)に対する単純なカウンターなどではなく、そこにはプログラムデザインについての重要な論点が含まれているように思える。

Is TDD Dead? 会談は、2014年の5月から6月にかけて、TDD提唱者の Kent Beck 氏と今回の騒動の主である DHH 氏、そしてホスト役である Martin Fowler 氏の三者で行われた。全5回の会談の内、今回は第二回までの内容を検討してみたい。

結局のところデザインの意思決定を行うのは開発者自身である

まず全体的な印象として、DHH氏と、TDD肯定派のBeck氏、Fowler氏の意見が真正面から対立しているという感じはそれほどなく、お互いに別のところを見ていて話がすれ違っているような印象を受けた。James Shore氏の以下のツイートは、この件に関するTDD肯定派のフラストレーションを代弁している。

DHH氏の想定するようなTDDは、Beck氏やFowler氏の想定するTDDとは異なり、単にやり方が悪いからうまく行かないだけで、DHH氏の発言は「わら人形論法」に過ぎないというのが、TDD肯定派の反論である。それに対して「うまく行かないのはやり方が間違えているからであって、正しく実践すればいつかは正解に辿り着ける」というのは「faith-based TDD」という信仰であって、そのようなものは認められないというのが DHH氏の立場であり、ここがすれ違いの中心になっている。

例えばBeck氏は、DHH氏のTDD批判は、車を運転していて見当違いな場所に行ってしまったからと言ってそれを車のせいにするようなものだと言う。TDDがプログラムデザインに悪影響を与えると言った場合、デザインの意思決定を行っているのはプログラマ本人であり、TDDがデザインを決定しているわけではないと。それに対してDHH氏は、TDDを実践する事によって、あるいは実践しようとすることによって、プログラムデザインにある種の傾向が現れるのは否定出来ないと反論する。

「結局のところデザインの意思決定を行うのは開発者自身である」という話については、「TDD再考」の第二回で紹介した James O Coplien氏も同じような話をしていた。

TDDを実践すれば自然にプログラムデザインが改善するなどということはあり得ず、良いデザインを実現するためには結局のところ自分の頭で考えなければならないというのが、Coplien氏の場合はTDDを批判する根拠の一つになっていた。ところが、TDD提唱者のBeck氏や、肯定派のFowler氏も、この件に関しては全く同じように考えており、TDDというのは彼らにとっては思考を助ける道具に過ぎないと発言している(なので適材適所で使う場合もあれば使わない場合もある)。つまり本当の対立は、TDDを肯定するか否定するかという部分にあるのではなく、それを使えば自動的に結果が好転していくような銀の弾丸として祭り上げている勢力とそれ以外の対立という事で、このブログで繰り返し取り上げてきた問題と全く同形の問題のように思える。

凝集性と結合性は頻繁に対立する?

第二回目の会談では、「TDD再考 (6)」でも紹介した、TDDが及ぼすプログラムデザインへの悪影響について検討を行っている。Railsのようなフレームワークを利用した開発で(厳密な)TDDを実践しようとすると、各層を単独でテスト出来るようにするために間接層(indirection)やモックオブジェクトを導入する必要が出てくる。それが結果としてコードを複雑にし、メンテナンスを難しくするというのがDHH氏の指摘だ。もちろん間接層が絶対悪というわけではなく、間接層にも依存モジュールを置き換えやすくするというメリットがあるわけだが、それをテストをやりやすくするという目的のためだけに導入するのは間違っているのではないかという指摘である。

この一連の話の中で最も興味深いと思ったのが、「凝集性と結合性は頻繁に対立する(cohesion and coupling are often opposed)」というDHH氏の主張である。

凝集性と結合性、あるいは凝集度と結合度は、プログラムデザインの良し悪しを計るための最も基本的な尺度だと言われる。凝集度はより高い方が望ましく、結合度はより低い方が望ましいとされる。モジュール間の依存関係はプログラムの構造的にはっきりと見えるので、結合性というコンセプトは比較的理解しやすいが、凝集性というのは具体的にどのようなものを指しているのだろうか?

凝集性・結合性は、多くの場合、相関があるとされる。具体的には「凝集度が高いモジュールは他との結合度が低いことが多く、逆に凝集度が低ければ結合度が高くなる傾向がある(Wikipediaより)」と言われている。しかし、DHH氏は「凝集性と結合性は頻繁に対立する」と言う。

DHH氏の主張を簡単に図解してみよう。

layer

Railsのようなフレームワークには、モデルとデータベースを密結合させた Active Record という仕組みがある。アプリケーションロジックを担当するコントローラは、この Active Record を直接操作するため、そのままだとデータベースにアクセスせずにアプリケーションロジックをテストする事は出来ない。そこでまずはアプリケーションロジックをコントローラから切り離して Action Runner というオブジェクトに移してフレームワークへの依存をなくし、その上で Action Runner は Repository という間接層を経由して Active Record の機能にアクセスするようにする。このようにすればアプリケーションロジックを Rails の枠組みに載せる事なくテスト出来るようになるというわけである。

この改造によって、確かにレイヤー間の結合度は下がったと言える。しかし、それと引き換えに凝集性が犠牲になっているというのが DHH氏の主張だ。ここで言う凝集性とは具体的に何を指しているのだろうか?

論理的凝集と機能的凝集

「凝集性」という日常的に馴染みのない言葉だと分かりづらいが、凝集性とは平たく言えば「まとまり度合い」のことを指す。例えば、今回問題にされているレイヤーアーキテクチャ一般について考えてみると、レイヤーアーキテクチャとは、アプリケーションの構造の中での役割分担を見出して、それらを疎結合となるように分割したものだと言える。分割されたそれぞれの層は単一の役割を引き受けており、その意味では全体としては凝集性が高くなる、つまりまとまりのある構造になっているはずである。

実は上のような凝集性を、分類的には「論理的凝集(Logical Cohesion)」と呼ぶ。論理的凝集とは、プログラムの構造、あるいは技術的な特徴によってまとまりを作ることであり、論理的凝集の尺度で言えば、レイヤーアーキテクチャの凝集性は高いことになる。

そのように考えると、DHH氏の言う凝集性は論理的凝集ではなく「機能的凝集(Functional Cohesion)」のことを指しているのだと思われる。この論理的凝集と機能的凝集の違いをクリアに説明している素晴らしいコメントが StackExchange にある。

論理的凝集は機能的な特徴よりも技術的な特徴でグルーピングを行うという意味であまり望ましくないと言える。… 例えば、データアクセスを行うモジュールをグルーピングすれば、それは論理的凝集を実現したことになる。… しかし、実際にモジュールの境界を決めるのは技術ドメインではなくビジネスドメインなので、論理的凝集はここで問題となる。論理的凝集を実現することによって、結果的に機能的凝集を失う事になる。

機能的凝集はビジネスドメインの概念によって決まるので、先ほどのレイヤーアーキテクチャの例で言えば、レイヤーを分割する事によって、ビジネスドメイン上の一つのコンセプトが複数のレイヤーの間に引き裂かれて分散してしまうことになる。

cohesion

一つのビジネスオブジェクト上の変更が複数のレイヤーにまたがって影響を及ぼすというケースは、フレームワーク上で開発する多くの人が経験しているのではないだろうか。

Cohesion と Coherence

凝集性は、英語では「cohesion」と呼ばれている。この言葉には以前から違和感を感じていたのだが、cohesion はライティングにおいて「文と文との繋がり具合」を指す言葉である。これは単に文の論理的なつながり、つまり構造的なまとまり表すだけで、意味のまとまりを表すのには別に「coherence」という言葉がある。どちらも「まとまる」という意味の「cohere」から来ているようであるが、それぞれ「構造的なまとまり」と「意味的なまとまり」で指す意味が微妙に違う。

ソフトウェアデザインにおける cohesion も、あくまで構造的なまとまり、つまり論理的凝集を指す言葉とし、機能的凝集については coherence という言葉を当てた方が良いのではないだろうか。というのは、上の話を鑑みても、この二つを言葉(コンセプト)としてはっきりと区別しておくことが重要であるように思われるからである。

TDD再考 (7) – テストはどこまで書けば良いのか?

「テストはどのレベルまで書けば良いのか?」という Stack Overflow の質問。明快な答えが出るような質問ではないので Stack Overflow ではオフトピックだとして既にロックされているが、「歴史的な意義がある」ということで残されている。

なぜ歴史的な意義があるかと言えば、TDDでテストを書こうとする人は誰でも抱くであろうこの疑問に対して、Kent Beck氏自らが回答しているのがその理由の一つだと思われる。そして、その回答が聴衆にとっては少なからず意外な内容だったことも、この質問の重要性に寄与している。

Beck氏の回答はシンプルである。

「私は機能するコードに対してお金を貰っているのであって、テストコードの対価を受け取っているわけではない。なので自分の哲学としては、テストにかける労力は必要な自信を得るための最小限度に抑えることにしている」

「必要な自信を得るための最小限度」は人によって異なるだろうし、チームで開発する場合は、チームを構成するメンバーのスキルや性格に依存することになる。つまり、完全にその人(チーム)次第になるということだ。

これはアジャイルの思想からすれば自然なことに思えるが、Beck氏の回答に対する反応を見る限り、少なからぬ驚きをもって受け止められているようである。「Kent Beckがこんな考えを持っていたなんて青天の霹靂である。TDDの信奉者は100%あるいはそれに準じる高いカバレッジ率を目指す傾向があって、Kent Beckも当然そうしていると考えているはずだ。」

Beck氏の回答に対して正面から反対している人もいる。コードは書いた本人だけでなく、事情を知らない他人がメンテナンスする可能性があり(そしてその人はその時点でチームのメンバーではないかもしれない)、当事者だけの文脈でテストの範囲を決めるのは良くないという指摘だ。あるいは、TDDはテストファーストが前提なのだから、一部分だけテストするということがそもそも成り立たないのではないか、等々。

別の回答では、TDDのT、つまり「テスト」という表現が「テストはきちんとやらなければいけない」という、TDDの本来の目的とは異なる品質保証の文脈を呼び込み、その結果としてカバレッジ云々の話になってしまうのではないかと指摘している。TDD界隈では頻繁に耳にする指摘である。その反省を踏まえて、BDD(Behavior Driven Development)というものが生み出された。

TDD再考 (6) – テスト容易性 ≠ 良いデザイン

ソフトウェア開発の世界にTDDが紹介された当時、最も説得力があり、そして魅力的に響いた格言は「テスト容易性(Testability)を追求すれば、自然に良いプログラムをデザイン出来る」というものだった。あるモジュールをユニットテストするためには、そのモジュールをシステムの他の部分から切り離して、独立して動作させる必要がある。ということは、テストしやすいモジュールは必然的に良いモジュール設計の指標である「低結合度(low coupling)」を実現することになる。

しかし、テスト容易性は必ずしも良いデザインに結びつくわけではないと、DHH氏は言う。テストをしやすくするために、過剰な「間接層(indirection)」が導入されることによって、コードの簡潔さが失われてしまうと。

過剰な間接層の例として紹介されているのが、Hexagonal architecture というデザインパターンで、アプリケーションのコアとなるドメインモデルと外部のインタフェースを明確に分離するためのテクニックである。階層化アーキテクチャの一種と言っても良い。ドメインモデルを「ポート」と呼ばれるAPI的な層(「ポート」という語はネットワークのプロトコル・ポートや電子機器の接続を行うポートを意識している)で取り囲み、さらにその外側に「アダプタ」と呼ばれる、様々な外部システムとのやり取りをポートに適合するよう変換するためのレイヤーを配置する。

Hexagonal architecture http://alistair.cockburn.us/Hexagonal+architecture

上図のシステムでは、administration, trigger data, notifications, database という4つのポートがあり、それぞれのポートに外部システムあるいはインタフェースごとのアダプタがある。

さらに今回の記事で紹介されているのは、Ruby on Rails の Controller や Active Record といったパーツを他のレイヤーに依存させずにユニットテストするためにこの Hexagonal architecture を導入するという例で、これは完全にやり過ぎであると、DHH氏は批判しているわけである。

それぞれのレイヤーを単独でテスト出来るようにするため、Controller からは直接 Active Record にアクセスさせずに Repositoryパターンを経由するようにし、Controller内のロジックは Commandパターンとして切り出す。つまり、ロジックを Rails に依存しないモジュールに配置して、それらを Rails から切り離して高速にテストできるようにするためのテクニックである。

DHH氏は、Hexagonal architecture自体を非難しているわけではなく、多種多様な外部システムに対応しなければならないシステムで役に立つ可能性については認めている。問題にしているのは、ただ「ユニットテストをやりやすくするため」だけの目的でこの複雑な構造を導入することについてである。つまり、間接層を導入してモジュールの置換可能性を高めるのは、機能的な要求があれば合理的な選択となるが、ただユニットテストのためだけにこれを導入するのは、犠牲にされたプロダクトコードの簡潔さとその見返りの釣り合いが取れないのではないかという指摘だ。

そもそもControllerの責務は、ユーザーからのリクエストとモデルからのレスポンスを統合することなのだから、ユニットテストではなく結合テスト(Integration test)で対応するべきだ、というのもなかなか説得力がある。そのように考えると、システムの中でユニットテストの対象となる部分はかなり限定されることになりそうだ(ユニットテストの定義をどう捉えるかによるが)。ドメインモデルのテストについても、データベースから切り離してテストしようとするのは時代遅れであると切り捨てている。これは筆者も同意する部分で、モデルにおいてはデータベースとのやり取りがむしろ一番重要なところであり、昨今のORMではインメモリデータベースを利用して簡単かつ比較的高速に結合テストが実施出来るため、これをやらない理由はほとんど無いように思える。

さて、TDD陣営の人たちはこの過剰な間接層問題に無頓着だったのだろうか? 過去の資料を当たる限りそんなことはなさそうだ。Martin Fowler氏による有名な書籍『リファクタリング』の中に、Kent Beck氏による「間接層とリファクタリング」というコラムがある。

「コンピュータサイエンスは、間接層(indirection)を設けることであらゆる問題が解決できるという信念に基づいた学問である。― Dennis DeBruler」

TDDに欠かせないプラクティスであるリファクタリングは、つまるところプログラムに間接的な部分を加えるということである。この間接層というものの重要性を強調しつつも、

しかし、間接層はもろ刃の剣であることに注意しなければなりません。1つのものを2つに分割するということは、それだけ管理しなければならない部分が増えるということなのです。また、オブジェクトが、他のオブジェクトに委譲を行って、その先もさらに委譲を繰り返すような場合、プログラムが読みにくくなるのも事実です。つまり間接層は、最小限に絞り込むべきなのです。

TDD再考 (7) – テストはどこまで書けば良いのか?


TDD再考 – これまでのコンセプトマップ

これまでの資料まとめ - https://oinker.me/room/marubinotto/TDD
これまでの資料まとめ – https://oinker.me/room/marubinotto/TDD

TDD再考 (5) – ユニットテストの「ユニット」とは何を指すのか?

この「TDD再考」シリーズの第三回で「TDD is dead」に対する Kent Beck氏の反応を紹介したが、今回は Martin Fowler氏の反応について検討してみたい。この後、David Heinemeier Hansson, Kent Beck, Martin Fowlerの三者は「Is TDD Dead?」という対談を展開して行くことになる。

「TDD is dead」が公開されて激しく燃え上がっている中、Fowler氏はまず「UnitTest」というタイトルの記事を自身のサイトに投稿している。

この記事の中でFowler氏は、DHH氏の記事の内容には直接触れずに、そもそもユニットテストの定義って何なのだろうという話を展開している。それは、DHH氏やJames O Coplien氏の批判の矛先がユニットテスト(の価値)にあり、議論に先立ってまずユニットテストってそもそも何なの? という部分をはっきりさせておきたいということがあったのだろう。

まず、ユニットテストと言った場合、それが低レベルかつシステムの中の小さなパーツを対象としていること、基本的にはプログラマー本人によって書かれること、といった部分については多くの人の同意を得られそうであるが、それとは反対に、最も意見の食い違いが発生する部分が「コラボレーター」をどのように扱うかという問題だと言う。

とあるモジュールをテストする場合、そのモジュールが単独で機能するケースはどちらかと言えば少なく、何らかの依存するモジュールが必要となる場合が多い。この依存モジュールを「コラボレーター」と呼ぶ。ユニットテストは「ユニット」つまり単体のモジュールを対象としているのだから、コラボレーターはテストの対象から切り離すべきであり、そのためにモックオブジェクトを積極的に利用すべきだというのが Mockist style と呼ばれる勢力で、反対にコラボレーターも一緒にテストしてしまって問題ない、あるいはむしろそうすべきだと考える勢力を Classsic style と呼ぶ。

Fowler氏は Classsic派だったので、コラボレーターを切り離さないのはユニットテストの定義から外れるのではないかと批判されたそうだが、テストのターゲットはあくまでユニットの振る舞いであり、その内部構造は問題でないと考えているようだ。

Indeed this lack of isolation was one of the reasons we were criticized for our use of the term “unit testing”. I think that the term “unit testing” is appropriate because these tests are tests of the behavior of a single unit. We write the tests assuming everything other than that unit is working correctly.

ここで筆者はうーんと考え込んでしまった。

Fowler氏自身が書いているように、何をユニットと見なすかは状況に依存するので、いわゆる振る舞いがユニットを形成してさえいれば、その内部構造がいかに複雑であっても、それをユニットと見なせる可能性があるということだ。これはシステムというものが基本的には入れ子の構造になっていることを考えると当然のことだとも言える。概念的には入れ子のどこを切ってもユニットになり得る。

となると、もはやユニットという言葉に意味はないような気がしてくる。それよりも、どのようなタイプのインタフェースに対してテストをするのか(プログラムモジュールなのか、REST APIなのか、UIなのか)、コラボレーターのセットアップにどれぐらいの手間や時間がかかるのか、そしてそのコストはプロジェクトにとって許容範囲内なのか、というような指標の方がテストというものを分類するためには適切なように思える。

TDD再考 (6) – テスト容易性 ≠ 良いデザイン

TDD再考 (4) – TDDはプログラム初心者にとって自然な方法になり得るか?

前回取り上げた、TDDが解決しようとした問題の一覧を眺めていると、それらの問題は Dave Thomas氏が言うところのアジャイルの価値とも共通する部分が多く、TDDというのはアジャイル手法全体のメタファーとなるような、言わばミニアジャイルのような存在と言っても差し支えないように思える。

間違っているかもしれないが、アジャイル以前から長くプログラムを生業としていた人ほど、テストファーストという考え方に抵抗感を抱くのではないだろうか?

TDD、あるいはアジャイルという考え方は、外部からの要件(「機能」)が、結果として、その機能を実現するために必要な「構造」を生み出す、という考え方である。これは、この命題だけに注目する限りは当たり前の事実のようにも思える。しかし、プログラマは、経験を積み重ねる内に同じような構造のパターンに何度も遭遇することになる。そうやって頭の中に有用なコンセプトが蓄積されて行く。それらがイディオムとかデザインパターンなどと呼ばれたりする訳だが、TDDの問題意識は、それらのコンセプトの適用範囲は思ったほど広くなく、そして耐用年数は思ったよりも短く、濫用すれば過剰性能を作り込む罠に落ちるというものであった。

しかし、人間はどうしても知識を貯えてしまうので、解法を先に考えてしまうというバイアスから逃げるのは至難の業である。そしてこれが、経験者ほどテストファーストに抵抗感を抱くのではないかという、推測の根拠である。

要件が構造を生み出す、というロジックはとても反論しづらい。何のためにソフトウェアを作るのかということを考えれば、それが至極当然の流れのように思える。ミニアジャイルであるTDDがプログラマの間でドグマ化してしまうのも頷ける気がする。

では、経験者のバイアスが存在しない初心者のプログラマにとって、TDDは自然な手法になり得るのだろうか?

TDD以前では、プログラムデザインのフォーカスはやはりプロダクトコード側にあって、より良いモジュール、あるいはより良いオブジェクトのデザインは何かということについて、様々なデザイン原則やパターンなどを学習し、デザインの質を高める事によって、プロダクトの保守性や信頼性を担保するという方法が一般的であった。

TDDの出現によって、プログラミングのフォーカスは、プロダクトコードからテストコードに移動する。テストをしやすいようにプロダクトコードをデザインすると、結果的に、モジュール間の疎結合を実現でき、自然によいデザインへと導かれるというわけだ。テストコードを検証すれば要件との齟齬をチェックでき、テスト自体を実行してプロダクトコードの正しさも確認出来るという、まさに一石二鳥・三鳥の方法であった。

実際に筆者も以前、初心者はTDDのやり方から学べば、自然にプログラムデザインも学べるし、管理者視点からは要件との齟齬もチェックしやすく、なかなか良いアプローチではないかと考えていた時期もあった。

しかし、実際には様々な障害があることが分かった。例えば、テスト駆動で小さなステップを踏むことの難しさである。

think of a test that will force you to add the next few lines of production code. This is the hardest part of TDD because the concept of tests driving your code seems backwards, and because it can be difficult to think in small increments. (James Shore: The Art of Agile Development: Test-Driven Development)

TDDでは、API視点からプログラムをデザインすると言っても、実際はインクリメンタルに開発するために、どういうテストを書けばプロダクトコードの変更を最小限に出来るかというアクロバティックなことを考えなければならない。これは、TDDに限らず、vertical slice単位で開発するアジャイルの難しさの中心的な課題といってもよく、とても初心者が太刀打ち出来る問題とは思えない。

もう一つ例を挙げると、James O Coplien氏の指摘にも通じるが、自動テストが(現実的に)カバー出来る範囲というのは思ったより広くない、ということである。

これはつい最近、たまチームで遭遇したばかりの出来事だが、テスト(外部)からは期待通り動作しているように見えるのだが、実はプラスアルファの余計なことをやっていて、関係ないデータを壮大に破壊していたという事例があった。原因は、ライブラリのAPIについての理解が足りなかったという、ごく初歩的な問題である。こういった問題を考えると、テストプログラムで担保出来る信頼性など、特に初心者を対象とした場合、取るに足らないことが分かる。まさに、Coplien氏が言うところの「テストがコードのクオリティを上げる訳ではない、上げるのは開発者自身である」。

TDD再考 (5) – ユニットテストの「ユニット」とは何を指すのか?

TDD再考 (3) – TDDが解決しようとした問題は何か?

DHH氏の「TDD is dead」を受けて、TDDの生みの親である Kent Beck氏が Facebook に投稿した記事。

いささかアイロニカルな反論ではあるが、TDDが解決していた(とBeck氏が考える)問題の一覧が挙げられているので、以下にまとめておく。

  • 過剰性能(Over-engineering)
    • どうやって作り過ぎに対応するか?
    • アジャイルでいう YAGNI
    • 要件を満たす最低限の実装に留める(Just enough)
  • APIフィードバック
    • API設計の妥当性をどのように検証するか?
    • プログラムを利用者視点(API視点)でデザインできる
  • 論理エラー
    • コンパイラでは捕捉出来ないエラーをどうやって発見・防止するか?
  • ドキュメンテーション
    • プログラム(API)の利用方法をどのように伝えるか?
  • 大きな問題
    • 一度に解決しようとするとハマってしまいそうな大きな問題にどう立ち向かうか?
    • 簡単に解ける小さな問題を積み上げていく(小さなステップ)
  • インタフェースと実装の分離
    • どうやってAPIが実装依存にならないようにするか?
  • 問題共有
    • どうやってプログラマ同士の問題共有を正確に行うか?
  • 不安
    • どうやってプログラムが期待通りに動作している事を確認するか?

このようにして、TDDが解決しようとしていた問題点を並べてみると、問題そのものの妥当性にはそれほど異論はないように思えるが、その解決策については必ずしもTDDに限る必要はないようにも思える。前回の記事に書いたように同じ価値を実現できるなら、必ずしも誰もが同じ方法をとる必要はない。

テストを先に書かなくても、一つ一つのステップを小さくする事は出来るし、小刻みな Test-Last アプローチでも、API視点でプロダクトコードを検証する事は出来る。エラーの抑止、リグレッションテスト、あるいはドキュメンテーションという観点から言えば、テストをいつ書くかは問題にならないはずである。

あるいは、DHH氏やJames O Coplien氏が指摘しているように、ユニットテストにフォーカスし過ぎる事は、より重要で本質的な問題、つまりビジネス価値の軽視につながる可能性もある。これは、TDDの手法そのものではなく、その問題設定に対する疑義である。

TDD再考 (4) – TDDはプログラム初心者にとって自然な方法になり得るか?

TDD再考 (2) – 何故、ほとんどのユニットテストは無駄なのか?

TDD is dead」の最後で紹介されている、TDD否定派として有名な James O Coplien氏の論文。なかなか読み応えがある。

ユニットテストには基本的に価値がないので、よりビジネス価値を反映する粒度の大きなテスト(functional tests, integration tests, system testsなど)に軸足を移すべきである、というのがこの論文の論旨。

何故ユニットテストに価値が無いのかという部分の説明についてはなかなか説得力がある。論点を挙げてみると、

  • TDDの実践者が言及するテストカバレッジ100%には意味が無い
  • バグを取り除く最大の機会はテスト以外のところにある
  • ユニットテストにビジネス価値はほとんどない
  • TDDはプログラムデザインに害を及ぼす
  • ユニットテストの維持コストは馬鹿にならない
  • テストがコードのクオリティを上げる訳ではない、上げるのは開発者自身である

一つ一つの論点を追いかけてみよう。

テストカバレッジ100%には意味が無い

Coplien氏によれば、一般に言われるカバレッジ100%というのは、計算理論の見地から言えばまったくのナンセンスであり、本当の意味で100%を目指すのならば、到達可能なパスの全パターンと、その時にアクセス可能なデータの全パターン、それら全部の組み合わせを検証しなければならないとのこと。そうでなければ、正式にそのプログラムが「正しい」とは言えないのだそうだ。

TDDのメリットとしては、もちろんプログラムの信頼性向上も謳われる事が多いが、いわゆる品質保証としてのテストとは区別される事がほとんどで、TDDとしてのフォーカスはプログラムデザインに当てられる事が多いように思う。なので、この批判はTDDに対する攻撃としてはそれほど有効ではないように思える。

一方、ユニットテストを品質保証の根拠として捉える向きには、計算理論的にコードの正しさを証明する事は非現実的であり、ほとんどのケースでは恣意的な基準によって判定されていることを明らかにした、ということになるだろうか。しかし、果たして未だにそのような勢力(ユニットテストを品質保証の根拠として捉える向き)が存在するんだろうか…?

それとは別にして、プログラマの仕事をメトリクスで計ろうとする事の落とし穴については注意深く考えた方がよいだろう。筆者自身も過去のプロジェクトで、テストプログラムを書いた経験のないプログラマが、カバレッジを基準値まで上げるために、ただプログラムを実行するだけで Assertion が全くないようなテストプログラムを書いているのを目撃した事がある。

バグを取り除く最大の機会はテスト以外のところにある

これはウォーターフォール時代から言われていた事だが、プログラムの品質にもっとも重大な影響を及ぼすのは、要求・ドメインの分析結果を設計に落とし込むタイミングである。

その前提の上で、ユニットテストという高コストな手法でバグを潰すのは、果たして費用対効果的にどうなんだろうかという話である。

TDDも設計手法ではあるが、Coplien氏は他の論点でも述べているように、TDDはプログラムデザインに害を及ぼすと主張している。なので、プログラムの信頼性を向上させる手法として、TDDに従来の設計手法以上の価値があるとは認めていない。「コードのクオリティ向上に寄与するのはテストではなく、結局のところそれは開発者自身である」というのが、彼の基本的な立場である。

ユニットテストにビジネス価値はほとんどない

おそらくこれが一番重要な論点だと思われる。Coplien氏によれば、ビジネス要件から導出されたテストだけがビジネス価値を持つ。ほとんどのユニットテストは、ビジネス価値とはあまり関係のない開発者の独断によって設計されている。

もちろんユニットテストにビジネス価値と直結する部分が全くないとは言い切れない。例えば、キーとなるアルゴリズムを実装している部分などだ。しかし、基本的にビジネス価値はより粒度の高いテストで表現すべきである、というのがCoplien氏の主張である。

ユニットテストの価値を計る方法として、もしそのテストが失敗したら、どのようなビジネス価値を毀損することになるかを考える。もし、よく分からない、あるいは明確でない場合、そのテストのビジネス価値はほとんどゼロだということになる。価値が無いのにも関わらず、そのテストを維持管理するのにはコストがかかる。であれば、そのようなテストは捨てた方が得策である。

TDDはプログラムデザインに害を及ぼす

これはDHH氏も指摘していたが、TDDがプログラムデザインを改善するという客観的な根拠や事例はなく、それを調査した研究によればむしろ逆の結果が出ているようだ。

TDDが及ぼす悪影響として、プログラムを過剰に細分化してしまうため、ドメインとプログラムで表現の粒度が釣り合わなくなり、プログラムが理解しづらくなる事、ユニットレベルでのビジネス価値が小さくなってしまうこと、などが挙げられている。

しかし、仮にCoplien氏が言うように「テストがコードのクオリティを上げる訳ではない、上げるのは開発者自身である」のであれば、TDDがデザインに与える影響を客観的に計る事自体が無理筋であるように感じられるのだが、どうだろうか。

ユニットテストの維持コストは馬鹿にならない

この問題はTDD支持者の中でも多くの人が認識するところだと思う。

テストコードもプロダクトコードと同様(あるいはそれ以上に)、デザインとメンテナンスにコストがかかる。

特にテストコードの設計においては、プロダクトコードとは異なる方法論を採用する必要があり、これが想像以上に難しい。ただテストコードがあるというだけで良しとしているプロジェクトも多いが、テストの品質がプロジェクトに与える影響は想像以上に大きい。

プロダクトコードの設計を改善するためには、より良いテストの設計手法を学習しなければならないが、そのためには前提としてプロダクトコードの設計手法に精通する必要がある。プロダクトコードの設計手法に精通しているのなら、何故設計手法としてのTDDにわざわざ手を出さなければならないのかという矛盾がある。

プロダクトコードの coupling(結合度)とcohesion(凝集度)を改善するために、テストを書くというのがTDDであるが、テストコードがプロダクトコードに強く依存してしまう(高結合度)という問題がある。プロダクトコードに大きな修正が入った場合の、テストコードに及ぼす影響は甚大であり、粒度が小さく数の多いユニットテストのメンテナンスコストは想像以上に大きくなる。

テストがコードのクオリティを上げる訳ではない、上げるのは開発者自身である

ユニットテスト・TDD批判のベースにある考え方であり、これはある種の正論として多くの人が賛成出来る部分では無いだろうか。

Dave Thomas氏の「Agile Is Dead」宣言にも見られるように、いわゆる銀の弾丸的な存在としてアジャイルやTDDがあるように喧伝されるために、このような違和感を表明しておきたいというのは多分にあるのではないかと思う。

Coplien氏は、件の記事の締めくくりに、コンピュータ資源が豊富になって何でも簡単に試したりやり直したり出来るようになると、自分の頭で考えなくなってしまう(ツールのアウトプットを盲信してしまう)とう話をしている。

参考資料

James O Coplien氏のTDD批判については、氏に直接話を聞いたという安井力氏のブログとFacebook上のやり取りが大変に興味深かった:

TDD再考 (3) – TDDが解決しようとした問題は何か?

TDD再考 (1) – テストファーストとユニットテストへの死刑宣告

いかにも物議を醸しそうな(そして実際に醸しまくった)この記事を、Ruby on Rails の作者である David Heinemeier Hansson (DHH) 氏が発表したのが去年の4月、ということはあれから既に一年以上が経過している訳で、今更感が若干漂うところではあるが、このシリーズ「TDD再考」では、このDHH氏の記事に端を発して行われた様々な主張や議論を振り返りながら、アジャイル以後のプログラムデザインについて再考してみたいと思う。

件の記事でDHH氏は、「TDDは死んだ」と高らかに宣言しているわけだが、まず始めに気をつけなければならないのは、DHH氏が言うところのTDDというものが、かなり極端な例を念頭に入れて想定されているという事である。それは彼が「fundamentalism(原理主義)」や「fanatical(狂信的)」のような強い言葉を使っている事からも伺われる。

彼の批判を紐解くとポイントは二つある。一つはテストファーストこそがプログラミングのあるべき姿であり、プログラムのクオリティはそれをいかに正しく実践しているかによって査定されるという考え方が蔓延した結果、実際にそれが有効かどうかとは関係なく、それを実践しているかどうかで技術者の値踏みが行われるようなってしまったということ(ドグマ化)。そして、二つ目は、厳密な意味でのユニットテストでTDDを実施してプログラムデザインを導出すると、必要以上の細分化(これは James Coplien氏による指摘)と間接化(大量のMockオブジェクト)によって、過剰な複雑化を招くという事である。

そして、テストファーストという実践は捨てて、ユニットテストよりもシステムテストの方に重心を移すべきだと提案している。ユニットテストの対抗として挙げられているのが、データベースアクセスも含む形で Rails の Active Record をテストする例や、Webブラウザ経由のテストを自動化するフレームワーク「Capybara」などである。

実際にアジャイルやTDDの実践者の話を思い返しても、彼らがDHH氏が言うところの厳密な意味でのユニットテスト(依存関係は全てMockにして、対象となるユニットだけをテスト対象にする)にこだわっている印象はない。むしろ、DHH氏と同様、過剰なMockの導入は無駄が多いし、本来テストすべき箇所をテスト出来ないという問題意識を持っている人が多いように思う。Mockにこだわるのは厳密な Outside-in デザインにこだわる Behavior Driven Development (BDD) の一派ではないだろうか。実際にオリジナルTDD派とBDD派にはそのような対立も散見される。

TDDがドグマ化しているという指摘が、この記事の主題であり、多くの人の興味と共感を得るきっかけとなった部分だと思われるが、仮にそれが正しいとして、何故このようなことが起きたと考えられるだろうか? 

まず、TDDが提案した、プログラムデザインとテストを一体化するという考え方のインパクトが大きかった事が挙げられるだろう。ここにはドグマ化の要因となる二つの要素があったと考えられる。一つは、DHH氏も指摘しているように、これまでのソフトウェア開発の中で主要な不作為の一つだと見られていた、テストを書かない・しないということへの処方箋だったこと。専門家はこういった不作為を特に厳しく糾弾する傾向がある。そして、更に重要だと思われるもう一つの要因は、それが「要求駆動」のデザイン手法だったという事である。

テストをやらないというのは、将来起こりえる問題への不作為であるが、ここを過剰に追求するのはアジャイルとは矛盾すると考える事もできる。以前取り上げた記事の事例では、リーンスタートアップ的な実験フェーズにおいてテストを省略するという話が紹介されていた。

TDDが要求駆動であることの意味は大きく、それはアジャイルにとっても主題になるが故に、TDDがアジャイル実践の基礎だと捉えられている事が多い。そして要求駆動というのは、ユーザー中心設計や顧客中心主義のような、今の時代にとっての最大のドグマと言ってよい領域に属する。そして、これらの逆算的思考というのは本質的に反論がしづらい。

TDD再考 (2) – 何故、ほとんどのユニットテストは無駄なのか?