僕がTDDをやめた理由
タイトルは、まあ、半分釣り。TDDな人もそうでない人も、肩の力を抜いてお気楽にどうぞ。
本題に入る前に
まずお礼
ここで書くことは、前の記事 TDDはYAGNIに矛盾する? - カタチづくり から派生して色んな方と意見を交わした経験が元になっています。この場を借りて、色々とアドバイスを頂いた方に心から感謝の意を表します。
特にコメント欄にお寄せいただいた きしだ さんのコメントは、コメントと言うよりももはや一つの素晴らしい記事となっていて、もう必読といってもいいレベルじゃないでしょうか。本当にありがとうございます。特にBDDについて大きなヒントを頂きました。
押し付けではなく、交換
タイトルから想像がつくとおり、ここにはどうしてもTDDに対して否定的な意見ばかりが並んでしまう。でも、だからといって僕がTDDを完全に否定しているとは思わないで欲しい。
僕が今一番恐れていることは、TDDに対して否定的な意見を書くことで、TDDを実践されている方を攻撃しているように勘違いされること。そう勘違いされたら僕は非常に悲しい。だって、僕はTDDやアジャイルなコミュニティから本当にたくさんのことを学ばせてもらったから。
僕たちが仕事をするのは、お客さんのためだったり会社のためだったり同僚のためだったりするけれど、決してネットの向こうのプログラマに対してカッコつけるためじゃない。アジャイルコミュニティで胸を張るためにTDDをするわけじゃない。もしこれを読まれている方がTDDを実践することでお客さんから高い評価を受けているのであれば、そのTDDの価値はここで僕がちょっと否定したところで揺らぐものじゃないわけだ。
僕も同様だ。どんなにTDDの有効性についてアドバイスされたとしても、最終的にはそれがお客さんのため会社のためになるかどうかを自分で判断して、自己責任でやるかどうかを決める。当たり前のことだ。だから、会社も立場も業界も異なる人同士で、互いに価値観を押し付けあうことに意味はない。
価値観を押し付けるのではなく、価値観を伝え合いたい。価値観を交換したい。価値観が伝われば、何かが揺さぶられる。何かが変わる。そんな交換が出来たらいいな、と思っている。
それからね、自分の心理を問うてみるに、本当は僕はTDDがやりたいんだと思う。だって、僕は過去にかなりTDDをやろうと頑張ったし、そのために色んなリソースも投入したし、それでもうまく出来なかったことが悔しいんだよね。その悔しさが積もり積もって恨み言のようになっているところもあるけれど、その深層心理ではTDDがやりたいと思っているのかもしれない。
だからアホなことを書いていても、暖かく見守ってね(^^;
僕のTDD経験値
まず、僕のTDDの経験を書いておこう。TDDをやってみたこともないまま批判だけしているとは思われたくないので。
今更改めて紹介するのは恥ずかしいのだけれど、僕は過去にTDDについてこんなことを書いたことがある。
簡単にいうと、TDDの所作を身に着けるのはなかなか難しいので、その練習用のトレーニングメニューが欲しいなぁ、という内容。2つ目のリンクは、その練習メニューを自分で作ってみたもの。その当時は、練習メニューを自分で作って自分で練習したわけだ。
そこである程度の感触を得たので、会社のメンバーを説得しつつ徐々にプロジェクトでユニットテストを始めていった。
- まずは社内で利用するミドルウェア(フレームワーク)の開発プロジェクトで開始。メンバーは3人程度。
- チームリーダーになったので、そのときの受託アプリケーションの開発プロジェクトでも採用。メンバーは2人。
- 形状処理がややこしい低位のライブラリ開発で使用。これは僕一人で開発。
こんな感じの経験を元に、TDDに対して感じたこと思ったことを書き連ねてみたい。
テストしたい部分に限ってテストが難しい
バグは偏在する。変更も偏在する。バグも変更も、ある特定の一部に集中する傾向がある。これは多くの方が語っていることだと思う。そして、バグや変更が集中する箇所に限って、どうもテストが難しいと感じている。
UI
GUIのテストが難しいということは、すでに多くの方が指摘している事実だ。この問題の回避方法として代表的なものは、MVCパターンのようにGUI部分とモデル部分をレイヤーで分離して、モデル部分にのみテストを記述すれば良い、というものだと思う。
しかし、CADのGUIというのはかなり複雑だ。レイヤーで分割した設計にしても、結局GUI側のレイヤーにバグが集中してしまう、ということも珍しくない。
CADのUIはマウスイベントの処理が複雑なのだ。CADをご存じない方は、例えば Power Point などを想像すればいいと思う。ビュー上に配置されたオートシェイプに対して、実に様々なマウス操作が定義されている。どのタイプのシェイプが選択されているのか、に応じて処理は異なるし、シフトキーやコントロールキーが押されている場合でも処理は異なる。下手な書き方をしたらあっという間にスパゲッティコードの出来上がりだ。
そして、この部分の仕様変更も実に多い。この部分こそうまくテストが出来たらなぁ、と思う。だけど、なかなかテストを書くのは難しい。
グラフィックス
僕はOpenGLを利用しているのだけれど、OpenGL周辺もバグを生みやすい。OpenGLは状態遷移マシンとして定義されていて、状態がグローバルに変更されてしまう。だから、ちょっとした状態の変更が思わぬところに影響してしまうことがあって、これがバグの源となる。
これをテストしようとすると、OpenGLのレンダリングコンテキストを生成しなくてはならない。そのためには、ビューを画面に表示しなくてはならない。そうすると xUnit ではテストできない。
こういう場合、モックオブジェクトを定義するというのが教科書的な回答だろう。だけれど、OpenGLのモックを丸ごと定義しようなんて馬鹿げているよね。
形状処理
ここが技術の最大の売りとなる部分だし、ここがテストできたらその効果は大きい。そして事実、テストがうまく適用できる場面もたくさんあったし、そのときはユニットテストの威力を感じることができた。
だけれど、なかなかテストが難しい場合もたくさんある。例えば、一つは与えられた仕様が「あるエッジとエッジを曲面がイイ感じに滑らかに接続すること」みたいな感覚的な言葉で与えられている場合。思わず「こんな曖昧なのは仕様じゃない」だなんて文句もいいたくなるけれど、それを言ったら仕事がなくなってしまう。感覚的な言葉をコードに変換するのが僕の仕事なのだから。だけれど、この仕様のテストをするのはほぼ不可能に近い。
もうひとつは、入力データの多様性。形状処理というのは実に多様な形状を相手にする必要があり、思いもよらないデータが入力される場面が少なくない。よくありがちな、正の整数を仮定していたら負の値が入力された、程度の多様性ではないのだ。この多様性をすべてカバーするテストを書こうと思ったら、天文学的な数のテストコードが必要になってしまうだろう。テストが書ききれないということは、つまりは仕様が書ききれないということと同義でもある。浮動小数点演算の演算誤差の問題もあって、この問題は根が深い。意外に思われるかもしれないけど、3次元CADの内部というのは驚くほど「曖昧な」仕様で「なんとなく」動いている。
テストで品質は向上するか
TDDのテストは品質保証のテストとは異なるものだ、ということは知っている。だけれど、最終的に何らかの品質向上に寄与しなくては意味がない。
バグはテストを簡単にすり抜ける
TDDではこのように主張していると思う。
あえて意地悪な見方をすれば、この二つは矛盾がある。ユニットテストが品質の保証になっていないのに、どうして安心してリファクタリングが出来ると主張できるのだろう。
もちろんこれは揚げ足取りだ。例え品質の保証になっていなくても、ユニットテストすらない状態から比べればはるかに安心できるわけで、ユニットテストの有効性がないわけではない。
しかし、こんな恨み言をぼやきたくなるくらい、見事にバグはテストをすり抜ける。リファクタリングの結果、テストは通っていてもバグが出た、という場面には何度も遭遇している。本当にテストがあればリファクタリングは安心なの?とぼやきたくなることもある。
結局、設計が重要
どんなにユニットテストを書いても、設計がクソだといつまで経ってもバグは消えない。
逆に設計がキレイに決まると、殆どテストしていなくても一発で動いてしまう。初期段階で多少のバグを出してもすぐに収束する。
結局、品質を上げるのはテストではなくて設計。これが僕が経験から感じていること。
こう書くと、「TDDは品質保証の道具ではなくて、リファクタリングを繰り返して設計を洗練させていくためのツールだ。うまくTDDをやればキレイな設計が導出できる。」という主張が返って来るかと思う。「テストが設計を導出する」という主張だ。僕はこの主張に対して、半分同意、半分疑問、という感じ。確かに、美しい設計はテスト容易性(testability)が高いと思う。しかし、逆はどうだろう。テスタビリティが高ければそれは良い設計と言えるだろうか。また、テストに駆動されていれば自然と良い設計が導出できる、というほど設計は簡単なものなのだろうか。
テストでアーキテクチャが導出できるか
これはTDDでフレームワーク開発をしていたときに感じたこと。TDDとフレームワークの開発は相性があまり良くないと思う。つまりホットスポットが抽象クラスやインターフェイスで定義されているようなアーキテクチャの開発が、TDDでやろうとするとどうも窮屈になる。
TDDの手順というのは、まず動くコードを作って、そのテストを動かしながら徐々にリファクタリングで洗練させていくというもの。つまり、まずは具象クラスから始めて、徐々に抽象クラスやインターフェイスを導出させていく、という手順になる。
しかし、フレームワーク開発というのは、そもそもホットスポットをうまく設計・定義することが目的であるため、最初から抽象クラスを構想してしまうことが多い。それにフレームワークの設計ではもっと大きな視点あるいはメタな視点で構想する必要があるので、TDDの「具象から抽象に向かってリファクタリングで漸近していく」という思考に馴染まないように思う。設計が局所解にはまり込んでしまって、大域的な最適解に到達できない感じがする。
また、ホットスポットを含むアーキテクチャをテストしようとすると、モックオブジェクトが必要になる。しかしモックを定義してテストするのは通常のテストコードよりも若干面倒だ。しかも、アーキテクチャの設計という行為は漸近的ではない大域サーチであるので、設計が落ち着くまではインターフェイスの定義などがめまぐるしく変更される。そうするとそれに合わせてモックオブジェクトもめまぐるしく変更しなくてはならない。これではテストがむしろ設計の足枷になってしまう。
まあ、ここで書いたことは僕の「思考の癖」みたいなものが影響しているかもしれない。あるいはテストのスキルが足りなかったのかもしれない。特にモックを生成するためのライブラリも利用していなかったことが悪かったのかもしれない。(ちなみに当時はC++だったのだけれど、C++用のモック生成ツールってあったんでしょうか?)
さて、今はC#を使っているのだけれど、C#だとリフレクションを使ってもっと高度で柔軟なフレームワークが設計できる。一種のメタプログラミングになるのだろうか。こうなると更にテストは難しそうな気がしてくるのだけれど、どうなんだろう。
テストと経営的な視点
テストがYAGNIな状況はあると思う
僕は今、社員は自分一人だけという超零細企業で仕事をしている。
3年ほど前に当時の会社が子会社を新設して、僕はそこに転籍した。転籍とはいっても、その子会社の社員は僕一人だけである。半分起業したようなものだと思うし、それくらいの気概でやらないと続かないとも思っている。
そういう状態で、僕は一人でコードを書き始めた。親会社の事業と全く関係ないコードではないけれど、特に親会社から受注して書いていたわけじゃない。そのコードが外に売れなくてはダメなのだ。資本金は親会社が出してくれたけれど、何も結果が出ないまま資金が底をついたらそこで終わりだっただろう。
当時は、「一体何を作ればいいのか」でよく悩んだ。売れないものやニーズがないものを品質よく作っても意味がない。狙っている事業の方向性みたいなものはあったけれど、まだ漠然としていたし、その狙いがあっているかどうかなんて誰にも分からない。
そんな状況では、ユニットテストはYAGNIだと思ったし、今でもそう思っている。
先日のエントリでは、色んな方から「ユニットテストはいつでも必要」との意見を頂いた。だからYAGNIではないと。そのコードの納品先がすでに確定している受託開発ならば、確かにそうかもしれない。しかし、「今書いているコードがもしかしたら誰にも売れないかもしれない」という恐怖のなかで、ユニットテストはそこまで最優先にやるべきことなのか。
以前、このブログで次のエントリを書いた。
「私がこんな設計をしたら,社内で絶対通らないですよ」(技術者の一人)。「技術的にすごいと感じるところは一つもない。我々ならもっと安く作れる」(ある技術者)。MacBook Airの内部構成は,設計の未熟さを表しているのだろうか。
【MacBook Air分解その5】「外は無駄なし,中身は無駄だらけ」 | 日経 xTECH(クロステック)最大の無駄は「売れないものを作ること」という考え方もあるのではないか。
まだ売れるかどうか分からないものの品質を作りこむことは、徒労に終わる可能性がある。つまり「無駄」になる可能性がある。品質を高く、製造コストを低く設計するのが、設計の真骨頂だとは思うのだけれど、そもそもその商品が売れなければ、その努力が無駄になってしまう。
[software][misc]「無駄」とは何なのか - カタチづくり
当時僕が抱えていた問題は、ユニットテストで解消するような類のものではなかった。ボトルネックはそこじゃなかったのだ。そもそも売れるのかどうか、価値を生むものなのかどうかすら分からない状態だった。その状況で、品質にこだわっても仕方がない。
多少バグがあっても構わないから、とにかく動くものを作ってみる。動いたら外部の人に見せてみる。その人がどれだけ食いついてくるか、どんな質問をしてくるか、あるいは全く興味を持ってもらえないか、そういう反応を見ながら、次の開発を進めていく。そんな手探りの状態が暫く続いた。
今、ようやく落ち着いてきたところ。お客さんに恵まれて、売れるか分からないという恐怖からも解放されて、ようやく品質のことを優先的に考えられるような状況になってきた。だから、そろそろユニットテストのような自動テストツールの導入を検討するべきかな、なんて思い始めたところ。まだどうするか、全然決めていないけれど。
競争力の決め手の機能はテストが難しい
テストが難しい項目として、次の4つを書いた。
- UI
- グラフィックス
- 形状処理
- フレームワーク
そして、この4つはまさに僕にとっての競争力の源泉。勝負のポイント。テストが難しいからといって、ここでの勝負を逃げたら商売にならない。
人が嫌がるような、テストが難しく仕様も曖昧になりがちな部分こそ、差別化のポイントだと思っている。ここに突進してナンボだ。そう思っている。
人と同じことをしても勝負には勝てないんだよね。