Hatena::ブログ(Diary)

カタチづくり RSSフィード

2009-02-01

[]TDDYAGNIに矛盾する?

Joel on Softwareに面白そうな記事が載っていた。

とはいえ、どうも僕の英語力では完全な読解が難しい。会話を書き起こしたものなので当然ながら文体が会話調で、僕にはなかなか理解が難しいのだ。以下で僕の読み間違いがあれば指摘して欲しい。


さて、冒頭で Joel 氏はこう言っている。

Joel: There's a debate over Test Driven Development... should you have unit tests for everything, that kind of stuff... a lot of people write to me, after reading The Joel Test, to say, "You should have a 13th thing on here: Unit Testing, 100% unit tests of all your code."

From Podcast 38 – Joel on Software

The Joel Test を読んだたくさんの読者が、Joel Test の13番目の項目として "Unit Testing" を加えるべきだと書いてきているようだ。すべてのコードに対して100%ユニットテストを用意するべきだと。

これ、アジャイル派はどう思うのかな。Joel Test に 100% の Unit Testing を加えるべきだと思う?


さらに、氏はこう続けている。

the whole idea of agile programming is not to do things before you need them, but to page-fault them in as needed.

アジャイルの考え方とは、事が必要になる前に手を出すのではなく、必要に合わせて page-fault することだ。

ここで page-fault と表現するところがニクい。OSのページング処理になぞらえて、必要になるまで遅延処理する、という意味だろう。

これはまさに、YAGNI - You Ain't Gonna Need It の思想だ。そして、氏はすべてのコードに対してユニットテストを用意しなくてはならないという規律は、YAGNIの思想と矛盾すると述べているように思う。

つまりこういうこと。

ユニットテストというのは変更に対してアジャイルに対応するために用意するものだ。その変更の必要がまだないうちから、すべてのコードに対してテストを用意するのはYAGNIの思想に反するのではないか。そのテストコードこそ You Ain't Gonna Need It ではないのか。

これは面白い問題提起だ。そして、僕はかなりの部分で Joel 氏に同意する。

以降の部分で、氏はオブジェクト指向設計の設計原則(principle)を過度に強要することの問題点も指摘している。つまり氏は、TDD にしても設計原則にしても、開発プロセスに対して過度に一律にエンジニアリングの規律を適用するのはおかしい、と指摘しているのだと思う。


以下、簡単な覚え書き。

  • ユニットテストや設計原則がうまく機能するシステムとしないシステムがある。
  • うまく機能するもの:
  • しかし、すべての個々のクラスにまで一律に適用するのはいかがなものか
    • テストの記述コストと、得られるベネフィットが釣り合わない場合がある
    • ある種の設計変更をすると、それに伴って全体の一割に及ぶテストコードが壊れてしまうことだってある
  • 私見:敢えて反論するならば、Joel氏の意見からは「testing が開発を drive する」という視点が抜けているとは思う。
    • ただし、僕自身が試してみた経験では、そこまで testing が開発を drive してくれるという感覚は得られなかった。

きしだきしだ 2009/02/02 20:04 私の体験からすれば、red -> green -> refactoring の、リファクタリングのためにテストをしています。コードが常に進化する過程でエントロピーが増大しようとするので、何か追加(それは秒/分毎に起こる)する都度、エントロピーの増大を抑えるためにリファクタリングします。もちろんベックが言うように、「私が意図した通りにコードが動かな」「だからテストするまで完了したと思わな」ということもあります。
必要になるまでテストしなくていいはずだ!という意見に対しては、「その通りです。私はいつも必要だから、テストしています。」と返答します。
結局、アジャイルの活動は全部相互連動しているので、何か一つの活動だけ切り出して反論しても、あまり的を射ないんじゃないかと思います。
私のこの感覚がdriveにあたるのかは分かりませんが、テストが心地よいハートビートになっているという感覚はあります。

u_1rohu_1roh 2009/02/02 23:05 コメントありがとうございます。
僕はどうもTDDがうまく使いこなせなかったので、しっかり出来ている人がうらやましいです(^^;

ちょっと質問してもよろしいですか。気が向いたら教えてください。

Q. リファクタリングや仕様変更/設計変更が複数の(多くの)クラスにまたがってしまって、それによって多くのテストが壊れてしまうような場面はないのでしょうか。また、そのような場面において、テストを書かなければいけないというルールが逆にリファクタリングを億劫にさせてしまうことはないのでしょうか。

Q. まだ設計があいまいな段階でも、TDDで設計の輪郭をラフにスケッチしながら徐々に設計を固めていく、という書き方はできるのでしょうか。それとも、そのようなラフスケッチはUML等で済ませておくのでしょうか。この辺りが、僕がTDDで躓いた最大の原因です。

※ ちなみに、TDDで躓いた理由はもう一つあって、それは僕がやっている3D CAD分野は往々にしてテストを書くことが非常に難しいことです。例えば、「ある2つのエッジの間を、この面が滑らかにイイ感じに接続していること」という仕様(?)のテストコードを書くのはほぼ不可能に近い・・・。

きしだきしだ 2009/02/05 01:42 3D CADのような高度な専門分野?は未経験ですが、確かに大変そうだという印象は受けますね (^_^A;
私はEclipse RCPでC/Sのフロントエンドを作ることも多いのですが、GUIの表面そのものの自動テストは完全に放棄しています。そこで頑張る手法が存在し、それに取り組んでいる方が居ることも知っていますが、今の私のテスティングスキルでは労力に見合わないと思っているので、GUIから可能な限りロジックや状態を分離することで、テスト可能な範囲を極力広げるという戦略に割り切っています。この割り切りによって、それまで広い範囲にわたって自動テストを諦めていたのに対して、自信を持てるコードの範囲が格段に拡大したと思います。

ご質問について、長くなりますが...

Q. 仕様変更や設計変更で多くのテストが壊れてしまうか
A. 私が思うに、仕様変更や設計変更で多くのクラスにまたがって変更が発生することは、テストの問題というよりも設計の問題だと思います。もちろん、仕様変更や設計変更に強いようにテストを組織化して、影響を最小化する工夫もありますが、これは2次的な問題だと思います。以下はパーソナルな経験の範囲でしかありませんが...

実際にTDDで開発する時と、非常に小さなクラスを沢山連動させてシステム(サブシステム)を作ることが多くなります。書いているうちに、「このクラスはこれくらいにしといてやるか」とか、「この先は今考えたくないからとりあえずインタフェースを想定して別なクラスに押し付けるか」という怠惰な気持ちになる結果、クラスが小さくなります。これを綺麗に説明すると、テスト可能性を考えるとあまり大きくなる前にクラスの責務を別なクラスに切り出す戦略とも言えますが、TDDでやってると小さく浅い指向スタックで仕事をするようになるので、自然とクラスが小さくなるというのが本音かもしれません。この結果として、クラスがSRP(単一責任の原則)に適うように作られます。これは誰でも何も考えなくてもなる訳ではありませんが、ある程度の設計経験があれば自然となると思います。
SRPに適う十分小さなクラスの連携で機能が実現されていると、仕様変更や設計変更の影響が多くのクラスに影響を与える可能性は非常に小さいです。ほとんどの場合、特定のクラスの変更や、クラスとクラスを結びつける新しいクラスの導入や結びつけの変更で済みます。既存のクラスを分解することもありますが、抽象度によって分解したり、責任を分解する場合があります。この場合も、その分解に直接影響を受けるクラスは大抵数個であり、場合によってはIDEの自動リファクタリング機能だけで済みます。ほとんどのクラスには影響が出ません。そのわずかなクラスに対するテストだけが影響を受け、ほとんどのテストには影響が出ません。

テストの記述方法にも工夫があります。JUnit入門書にあるように、クラス単位でテストクラスを作り、メソッド単位でテストメソッドを作る方法はウマくないと思っています。BDDがそうするように、コンテキスト(状態)毎にテストメソッドをクラスでグループ化し、そのコンテキストでの(小さな)振るまい単位にテストメソッドを書くようにします。これによりテストメソッドの直交性が最大になり、仕様変更や設計変更に追従して変更しなければならないテストの数が細小になります。
私はJDaveというJUnitのBDD拡張を使ってテストを書くのが好きなのですが、JDaveを使わない場合でも同じような考え方でテストを組織化します。

Q. 設計があいまいな状態からTDDを始めて徐々に固めて行けるのか
A. 私が思うに、TDDは普通その状況で進めます。設計全体のビジョンを最初から持たないわけではありませんが、なるべくそれに固執しないようにしています。これまたパーソナルな経験の範囲でしかありませんが...

設計ツールとしてのTDDと、機能追加や変更時にどのように設計変更を扱うかという問題は、本質的には別問題だと思っていますが、そもそもリファクタリングというテクニックは、それをウマく繋ぎ合わせるためのものだと思います。TDDで設計の進化を問題にする場合、ファクタリングをいつするのか、どのようにするのかということを具体的イメージとして持っていないことが原因な気がします。
TDDは動作可能な設計をテストによって担保する設計ツールだと思いますが、リファクリングはテストがある状況での安全な設計の進化を担保するツールだと理解しています。従って、TDDによって作られる設計を段階的に高度化して行けるのかということについては、リファクタリングを正しく行えば全く問題なく可能だと思います(開発全体をイテレーティブに捉えており、これを手戻りだと叫ぶ人が居ないことが肝要ですが)。
初期のイテレーションでは非常にシンプルにシステムを設計していたけど、機能追加によってクラスが大きくなろうとすると、自然と分割したい気持ちになります(自然となるかどうかは臭いを感じる訓練ができているかどうかによりますが)。当初想定していなかった抽象が必要になったら途中から抽象化しますし、縮退していた責任が拡大して来て分割したくなったら途中から責任を分割します。こうした分割時は、そのイメージに合わせて先にテストを分割する程度になりますから、それ自体が大変なことにはならないです。また、このように常にシステムを小さいクラスの連携で動作するようにリファクタリングすることによって、上述したように次の機能追加や仕様変更時の労力を低く抑えることができます。
私が思うに、その都度小さく分解するリファクタリングをしないために設計負債が蓄積し、あるところでそれに我慢できなくなって「設計変更!」と叫んで革命的にコードをリライトすることが、結果的にテストを使い物にならなくし、TDDではテスト変更コストが高いという状況を作るのだろうと思います(いやそもそも小さく設計するという気持ちが無いのが問題なことも)。コツコツ返済するか、まとめて返済するかの戦略とも言えますが、私が思うにコツコツ返済の方が自己破産しなくて済むと感じています。
UMLによるラフスケッチが最初にあって、そのイメージの沿って設計進化するのであれば、それも悪くないと思います。ですが、そうしたものが無くても設計の進化はできます。コードに耳を澄まし、コードが要求してくる設計が正しい設計だと信じれば、あらかじめラフスケッチが無くても大丈夫です(コードが要求してくる設計は、設計者のスキルによって異なりますが)。
ただ、現在の設計に問題が無いかについてチームでいつでも話し合い、問題を見つけた人が居たらすぐに設計を改善できるという、健全なチームの文化は必要だと思います。
私の経験では、いつもちょっと先の設計ビジョンはイメージしています。それをフィクションとしてUML図に書くこともあります。ですが、今必要でない設計を先回りして作りたくなる衝動には抗うようにしています。それでもついつい無意識的に先回りして設計を過剰に複雑にしてしまうこともありますが、後になってやはり後悔します。

u_1rohu_1roh 2009/02/05 10:42 うわ!ものすごく丁寧に回答していただいて、本当に、本当にありがとうございます。
なんだか恐縮してしまいます。

> GUIの表面そのものの自動テストは完全に放棄しています。

私も、テストが難しい部分とテストが有効な部分を分けて考えて、有効な部分には積極的にトライしていくべきなんでしょうね。

> テストの記述方法にも工夫があります。JUnit入門書にあるように、クラス単位でテストクラスを作り、
> メソッド単位でテストメソッドを作る方法はウマくないと思っています。BDDがそうするように、
> コンテキスト(状態)毎にテストメソッドをクラスでグループ化し、そのコンテキストでの(小さな)
> 振るまい単位にテストメソッドを書くようにします。これによりテストメソッドの直交性が最大になり、
> 仕様変更や設計変更に追従して変更しなければならないテストの数が細小になります。

この部分が一番、なるほど!と思いました。BDDという言葉は今までも耳にしていたのですが、意味があまり理解できていなかったようです。
この辺りが私のテストのスキルが足らなかった部分と感じます。どの単位でテストするのか、をもっと考えてみるべきかもしれません。テストする、というよりも、どの単位で設計をドライブするのか、というほうがより正解に近いでしょうか。

>「設計変更!」と叫んで革命的にコードをリライトすること

# 某CMの「計画変更!」ですね(笑
いや、それはあんまりやらないです。自分はコードに関してはかなり潔癖症なほうだと思います。常に常にきれいな状態を保つように細かいリファクタリングは繰り返しています。(テストがないリファクタリングをリファクタリングと呼ぶな、という意見はあるかと思いますが)

たぶん、先述の「テスト記述方法の工夫」が欠けていた点が、以前挫折した決定的な原因だったようです。そのせいで、テストの書き換えが負担に感じたのだと思います。コーディングやリファクタリングしているとき、クラス単位やメソッド単位でテストを書いていると「俺はその単位で設計しているんじゃないのに!」という不満が出てきて、やめてしまったんですね。もちろん、そのときは感じた不満をこのようにシンプルには認識できていませんでしたが。

当時は、Hoge クラスのテストを HogeTest クラスに書いて、ファイル名も HogeTest で保存していましたが、BDDだとテストのクラス名やファイル名も HogeTest にはならないわけですね?クラス構造とは直交した、振る舞い単位の名前をつけてテストクラスを作ることになると考えればよいでしょうか?

ホント、参考になりました。重ねて御礼申し上げます m(_ _)m

きしだきしだ 2009/02/07 01:27 長くてすんませ (^_^A;
暇つぶしに長い返答するので、やられてくださいw

BDDのテスト構造についてはJDaveのHPの例題が参考になります。
http://www.jdave.org/documentation.html

class Foo仕様 extends Specification<Foo> {
class コンテキスト1 {
Foo create() {コンテキスト1の状況の作成} //setUp相当
void ビヘイビア1() {コンテキスト1における振る舞い1の検証}
void ビヘイビア2() {コンテキスト1における振る舞い2の検証}
}
class コンテキスト2 {
Foo create() {コンテキスト2の状況の作成} //setUp相当
void ビヘイビア1() {コンテキスト2における振る舞い1の検証}
void ビヘイビア2() {コンテキスト2における振る舞い2の検証}
}
}

ビヘイビアメソッド名は、「〜すると〜なる」とか「〜である」とか「〜すると例外」とか、(そのコンテキストで)どういう挙動を示すのかをストレートに説明的に名付けます。JDave HPのStackSpecは良い例です。

JDaveの方が1つのファイルにまとめられて便利ですが、JDaveが意図していることを理解すれば、JUnitでも同じようにできます。

class コンテキスト1におけるFooテスト {
@Before void setUp() {コンテキスト1の状況の作成}
@Test void ビヘイビア1() {コンテキスト1における振る舞い1の検証}
@Test void ビヘイビア2() {コンテキスト1における振る舞い2の検証}
}
class コンテキスト2におけるFooテスト {
@Before void setUp() {コンテキスト2の状況の作成}
@Test void ビヘイビア1() {コンテキスト2における振る舞い1の検証}
@Test void ビヘイビア2() {コンテキスト2における振る舞い2の検証}
}

普通にJUnitでBDDできて良かった〜!と初めて書いてみたときに思いました。このようにJUnitのクラスを分けることは、JUnitテストとしては異端なのかというと、そんなことは無いようです。というのも、Kent Beckも「オブジェクト作成はsetUpで行え。作成方法が異なるならテストクラスを分けろ。共通処理のために基底テストから継承しろ。」と助言していた記憶があります。「テストメソッドをビヘイビア単位にしろ」というKent Beckによる直接的な助言は記憶にありませんが、「何か一つ確認するためにテストメソッドを追加して行く」という助言はあった記憶があります。TDD提唱者は結局ほぼBDDをやっていたんだと思いました。

新しい振る舞いを追加する場合は、単純にビヘイビアメソッドを追加します。振る舞いが変わる場合、そのビヘイビアメソッドだけを変更します。他のビヘイビアメソッドのことなんぞ気にしません。この「他は気にしない」という状況が、「何か一つ変えたら面倒なことになる」危険性を小さくしています。オブジェクトの生成方法やコンテキストの定義が変わった場合も、それに対応する設定コードは1カ所しかないので、沢山のメソッドを直す必要がありません。責任の分割が発生したら、対応するコンテキストや関連するビヘイビアを、ごっそり違うスペックに移動させます。実際は、スペックファイルをコピーして、双方から関係ないところを間引くようにしています。その過程で、構造から根本的に見直になることはほとんどないです。

ちなみにソースのエンコードはUTF-8でメソッド名は日本語にしています。メソッドを日本語にするとJavadocで説明すべきことはほぼ無いのでJavadocはあえて書きません。これで結構生産性と可読性(スペース効率)が上がっています。これも実は、テストメソッドを十分プリミティブに小さく書くおかげでテスト説明が短くなるからです。一つのテストメソッドの中であれもこれも検証したのでは、こうは行きません。

くどいコメントを書かせていただいて、重ね重ねお詫びいたします m(_ _)m

u_1rohu_1roh 2009/02/07 21:54 いやはや、非常に丁寧なコメント、ありがとうございます!
BDDは「なるほど〜」という感じです。「コンテキスト」と「ビヘイビア」という考え方にTDDからの進化を感じました。
勇気と希望を頂いたので、近い将来またテストにトライする日が来るかもしれませんね。会社やプロジェクトの状況を見つつ、考えていきたいと思います。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証