テスト駆動型開発についての議論

http://blog.testdouble.com/posts/2014-01-25-the-failures-of-intro-to-tdd.html

3 comments | 2 points | by WazanovaNews 3年以上前


Jshiike 3年以上前 edited | ▲upvoteする | link

Test Double社がブログで、TDD (テスト駆動型開発) を教える場合のアプローチを提案しています。

TDDについて、同じ用語やツールを使っていても、「モックオブジェクトがありすぎて、ひどい。」「モックオブジェクトがあふれていて素晴らしい!」という異なる見解に至るケースがでてしまっているのは、理想的なゴールに至る道筋を統一したかたちで教えきれてないからだと指摘しています。

TDDの一番の効果はコードのデザインの改善であり、コードのクオリティの担保は、うまくいけば二次的な効果、まかり間違えば幻想になるとし、失敗に陥りやすい具体的なパターンとして挙げているのが、

Failure #1: Encouraging Large Units

「直接問題を解決する。」(つまりコードの正しさを担保する。)というアプローチでユニットの単体テストを書きはじめると、仕様が増える度に解決すべき問題が増える。つまりテストが増えていく。別のユニットをつくるという発想に至らず、最初のユニットのコードがどんどん肥大化してしまう結果になる。TDDにおいて、"red-green-refactor" と提唱されていても、リファクタリングのところは開発者自身の規律/プロ意識の問題だと放置されてしまうことが多い。

Failure #2: Encouraging Costly Extract Refactors

リファクタリングに自主的に取り組んだとしても、親ユニットから適切に子ユニットを抽出する作業は大変。複雑な親オブジェクトから、小さな子オブジェクトを切り出し、親オブジェクトが多少複雑でなくなるという状態にするだけで、かなりの分析と集中した労力が必要になる。

Failure #3: Characterization Tests of Greenfield Code

リファクタリングがうまくいったとしても、今度は新たにつくられた子ユニットに対応する単体テストがないので、それを準備することになる。しかし、この場合はテスト駆動型での開発ではなく、既に出来上がった子ユニットのコードに対する確認のテストを書くことになるので、TDDで最初からやるよりはクオリティが落ちる。つまり、無駄な作業になりがち。

Failure #4: Redundant Test Coverage

次に子オブジェクトの振る舞いを変更するような新しい要件を採用しなくてはいけなくなったとする。改修ポイントは、結合テスト、子ユニットの単体テスト、そして子ユニットのコードであるとすぐにわかる。しかし、親ユニットの単体テストに失敗するケースもでてくる。子ユニットの新しい振る舞いを考慮して、親ユニットの単体テストを修正することになるが、最悪の場合、「親ユニットの単体テストの失敗は、false negativeであり、バグを示すtrue negativeではない。」という事実がわからなくなり混乱してしまうかもしれない。[概念図]

Failure #5: Eliminating Redundancy Sacrifices Regression Value

そこで、親ユニットの単体テストを設計しなおすことにする。具体的には、子ユニットをテストダブルで置き換えた前提にするのである。[概念図] しかし、元々の親ユニットの単体テストは、コードの正しさを担保するアプローチで書かれているので、それを担当した人々は、「これでは何も確認できないテストになってしまう!」と不安がり、結局は結合テストまで全て終わらないと、その不安は払拭できなくなる。この時点で、モック賛成派とモック反対派にわかれて出口がなくなってしまうプロジェクトをよく見てきた。

Failure #6: Making a Mess with Mocks

テストダブルを利用するからと言っても、ロジカルな振る舞いとユニット間のコラボを定義する内容をまとめた単体テストにすると、読みづらく、理解しづらく、変更しづらいものになる。テストダブルを採用した場合に、「モックに頼り過ぎ。」と批判を浴びるのもこの事情からである。解決策としては、親ユニットのリファクタリング。つまり、親ユニットは、他の子ユニットとのコラボレーションのみの役割をもち、ロジックの実装はしない。最初の子ユニットで抽出してない親ユニットの振る舞いは、別の新しい子ユニットにロジックを切り出す。そして、親ユニットの単体テストはコラボレーションを定義したものに全て書き換える。しかし、この解決策は、かなり労力のかかるものである。 [概念図]

そこで、Test Double社は、

  • 親ユニットは、ロジカルな振る舞いを実装している二つの子ユニットに、依存する。
  • 親ユニットの単体テストは、二つの子ユニットとのやりとりを定義する。
  • 二つの子ユニットは、それぞれのロジックを担保する単体テストをもつ。

というのが上記#1-#6の結論なのだから、最初からそれを目指すアプローチを取ればよいという提案をしています。

  1. 新しい機能のリクエストを受け取る。
  2. 機能がどれだけ複雑であれ、なぜコーディングするのかをまず考えてみる。
  3. 新しい機能の要件に対して、何を実現するかの宣言をする。例えば、「このファイナンスシステムにおいて、与えられた月もしくは年の利益額の値を返してくるcontrollerアクションを実装する。」(ちなみに、ここでは結合テストの詳細には触れないが、この宣言に従って、コードの正しさを担保するアプローチで結合テストを書くようにすると、単体テストをここで推奨するアプローチで進める際に役立つ。)
  4. すぐにロジックを書きはじめるのではなく、どのようなオブジェクトがあれば理想的か考えてみる。例えば、「このcontrollerは、各月の売上の値だけを返してくる何かと、各月のコストの値だけを返してくる何かに依存するかたちならシンプルにできる。」これで、小さく、単一の目的をもった子ユニットをデザインできる。[概念図]
  5. 親ユニットのTDDでの実装からはじめる。まず、上記の4.で考えた子ユニットがあたかも既に存在するかのように、コラボレーションについてのテストを書く。 [概念図] このステップで有用なAPIを見つけることもできる。
  6. 上記の4.で思いついた全ての子ユニットに対して、5. の検討をする。子ユニットが増えすぎてパニックすると思うかもしれないが、それぞれが単一の目的をもった小さな子ユニットなので、気軽に追加/削除ができ、要件が変更になっても整理が簡単。[概念図]
  7. 一通り上記の作業が完了すれば、ツリーの末端の子ユニットからロジックの実装をはじめてみる。目的としては、コラボレーションを司るユニットをなるべく見つけることで、それにより末端のユニットがスコープを絞ったロジックを実装できるかたちになる。ロジックが実装された子ユニットの単体テストは、テストダブルが不要で、かつ、適切なインプットとアウトプットを確認するだけなので、かなりシンプルにできる。[概念図] このアプローチのメリットとして、労力のかかるリファクタリングのプロセスがないことにも注目してほしい。

この記事に対して、下記のような議論が欠けているのではないかという意見もあります。

  • TDDがよい考えであるのはどういう場合か?(例えば、プロトタイプとか、システムの構成の考えがまとまってないときとか)
  • どのテストが価値が高く、それをどう見つけるか?
  • テストが価値を提供できる別のパターン(テストに対応できるシステムにする。実装の初期の段階でバグを見つける。将来リグレッションテストをする場を提供する。デバッグができるようにする。コードが劣化しないようにする。など)、どんなテストがどんな価値を提供できるか?、いつテストのメンテを正当化できる程度の価値がなくなったと判断するか?
  • 数百件のテストを破棄しなくてはいけない大きなリファクタリングが必要になったらどうするか?(それらの単体テストを書き直す価値はどれだけあるのか?)
  • テストを書き、メンテするコストと、その結果得られる価値とのROI
  • TDDの失敗事例を網羅すること(単体テストはうまくいくが、システム全体は壊れている。モックが多すぎる。リファクタリングにコストがかかりすぎる。小さな部品が多くなりすぎてどれが何だか把握しきれない。)と、それを避けるもしくはコストを最小化する。
  • TDDは必ずしも、パフォーマンス / 可読性 / 柔軟性に直結しない。

#テスト

Jshiike 3年以上前 edited | ▲upvoteする | link

Twitterで@hidenorigotoさんがおススメされていた、Uncle Bobからの返答記事があったので、メモとして残しておきます。

http://blog.8thlight.com/uncle-bob/2014/01/27/TheChickenOrTheRoad.html

  • Failure #1のリファクタリングは、身をもって学ぶという必要な作業である。また、Failure #2の作業も、早めにかつ頻度をあげてやれば、たいしたことはない。
  • Failure #3のような単体テストの書き直しはしない。そもそも、全てのmethodやclassごとにテストするのではない。振る舞いを定義し、それを実装するmethodとclassをつくり、テストを用意する。肥大化してくればfunction/classを抽出し、publicメソッドでまとめ、それだけを呼び出すかたちとする。
  • Test Double社は、ウォーターフォール型TDDを主張しているが、前提としたオブジェクト構成が完璧でなければリファクタリングは発生する。
  • Uncle BobのTDDにおいても、事前にユーザエクスペリエンスとシステムアーキテクチャを整理しておくことは重要。
WazanovaNews 3年以上前 | ▲upvoteする | link

atmarkITに特集の記事がありました。http://www.atmarkit.co.jp/ait/articles/1403/05/news035.html

Back