【翻訳】テスト駆動開発(TDD)はもう終わっているのか?PART I | POSTD
http://postd.cc/is-tdd-dead-part1/
【翻訳】テスト駆動開発(TDD)はもう終わっているのか?PART II | POSTD
http://postd.cc/is-tdd-dead-part-2/
2014-10-27 – やっとむでぽん – TDDの経験と現状のアンケート
http://d.hatena.ne.jp/yach/20141027
アンケート自体は「TDDに関心がある人」のフィルターが掛かっているので、数値そのものは意味がないのですが、傾向がわかります。私の場合、JUnit が出た当時から使っているので(その頃は CppUnit でテストしてた)かれこれ10年ぐらいやってます。最初のテストフレームワークは、自前の Excel VBA で C++ の自動テストをする、という自作 xUnit もどきを作っていて、その直後、Ruby の UnitTest から入ったぐらいですから、かなり初期の頃の TDD から入っています。XP のプラクティスのひとつとして、テストフレームワークと自動テストは必須だったので、それを最初に取り入れます。あらゆるテストを xUnit でやろうとした頃もあるのですが、UI 系のテストは諦めることにしています。というのも、「作業量に見合わない」作業をやっても仕方がないですから、の結論に至ります。
自前のライブラリを作るときや仕事でも TDD を使います。いや正確にはテストファーストではないのですが、大抵はテストコードを書いてから、それに合わせるようにライブラリを作ります。このあたりは、自前の UIDD (ユーザインターフェース駆動開発)のひとつで「見た目使いやすいものが、後後になっても使いやすい」に基づいています。
さて、そんな中で記事のタイトル通り「TDD は死ねません」≒ 死ぬことができませんな話を書き下しておきましょう。
何のために TDD を使うのか?
「プログラマの精神の安定のため」ではあるのですが、仕事なので「作業量」を少なくするため、という目標を掲げます。トータルの作業量が増えてしまえば(保守やリリース直前の不具合対応も含めて)、テストをやっていても自己満足にしかなりません。ちまちま一行プロパティをテストするのも「単体テストの行数を稼ぐ」という目的は達せられるでしょうが、最終的な目標は達成できません。どうやったってテストに合格するテストコードは書いても意味がないんですよ。なので書きません。
テストコードが複雑怪奇になって、テストコードの保守が大変になって、オブジェクト指向もどきのクラスライブラリが氾濫して自動テストをやるたびに、テストコードをあれこれと変えてやらなければ進まないのであれば、ざっくりとテストコードは捨ててしまいましょう。最終的な工程になって、手作業で UI を操作しなければうまくテストができなければ、自動テストをちまちまメンテするのは止めましょう。もっと、やらないといけないことがあるでしょう?ってな話です。
いわゆる、xUnit 的なツールは「治具」です。最終的な製品には含まれないけど、製品を精度よく作る、効率よく品質を上げるための「使い捨てのツール」です。なので、プロジェクトの横断するような自動テストを妄想するよりも、プロジェクトに密着したテストツールを、プロジェクト単位で作るほうがベータです。所詮「治具」なのです。だから、プロジェクトに密着したときにはじめて効果があるし、逆に言えば、他で便利だからといって自分のプロジェクトで便利とは限らない。そういう場合は、捨ててしまうし、そういう意味では、TDD is dead. なプロジェクトもあってもおかしくないのです。
そんな訳で、効率よく製品を仕上げるために TDD を使う、ってのがひとつの到達点です。適用範囲があるってことですよ。
何故 TDD は死ねないのか?
簡単です。TDD で作ったテストツールは、製品/成果物を裏で支えている訳ですから、製品が生きている間、それに密着したテストツールは生き続けます。と同時に、製品が死ねば、テストツールも死ぬ訳です。
製品やライブラリに何か新しい機能を付け加えるとしましょう。それが、テストコードにかなうものなのか、テストコード自体を破壊してしまうものなのか、という問題があります。テストコードは、適用範囲が決まっている(修正以前の製品に対して、の責務しかない)ので、新しく追加した機能に対して、テストコードの振る舞いは未知数です。いや、正確に言えば、機能追加について、2つの方法が取れます。
- テストコードを破壊するような、機能を製品に追加する。
- テストコードに適うような、機能を追加する。
後者のテストコードに適うような機能追加は、比較的簡単です。また、こうすることが TDD を利用する最大のメリットでもあります。既存のクラスやライブラリを壊さないように「再利用できる」ように機能を追加してきます。なので、既存のクラスやメソッドの振る舞いを変えないように注意深く作っていきます。この方法は、土台を固めて上に付け加えるように作っていく方法なのですが、デメリットもあります。複雑な内容に複雑な内容を追加していくために、最終的に複雑すぎるものができあがってしまいます。初期設計が悪いと、べたべたと回避するコードやメソッドばかりがちりばめられてえらいことになります。
そんな訳で、私たちは TDD is dead. と叫んで、テストコードを捨てます。以前のテストは設計思想が古いので、すべてを捨ててテストコードを書き直さないといけなくなります。クラスの設計をちょいちょいと直したいのに、膨大なテストコードを時間をかけて直すなんてナンセンスです。
でも、ちょっと待ってください。以前のテストコードを捨て去っても、また新しいテストコードを書くこともできますよね。所詮「治具」なのですが、新しい設計のクラスに見合ったテストコードを書けばいいだけです。つまりは、テストコードは、設計が死ぬとテストコードも死ぬのです。逆に言えば、設計が生きている限りは、テストコードは生きています。
テストコードを減らす努力をする
ちょっと考えてみましょう。TDD is dead. と叫んで、テストコードを投げ出したくなる理由は何でしょうか? テストそのものが嫌い? TDD 自体が嫌い? それはそれでよいのです、テストが嫌いならば、テストの無い形で MDA スタイルで作ることも可能です(それはまだ、夢物語な時代ではありますが)。テスト自体をやめてしまう、という方法もあります。しかし、逃れられないテストであるならば、何かと省力化する、あるいは効果の高い方法を取るほうがよいのではないでしょうか?
効率的なテストコードは、パレートの法則よろしく、全機能の 20% にだけテストコードを書きます。微々たる些末なコードにまでちまちまとテストコードを割り当てておくには、プログラマの人生は短すぎます。ランダムテストで自動化するとか、コードレビューで回避できるものも多いでしょう。なので、ユーザーインターフェース(クラスやライブラリを使う側の視点)でテストコードを書いていきます。
使う側の視点でテストコードを書くと2つのメリットがあります。
- 利用しにくいメソッドやクラスを排除できる。
- ヘルプ/サンプルとして利用できる。
既存のクラスライブラリを使うときに、あれこれと前提の設定があって、こっちのメソッドを先に呼ばないと次のメソッドを呼び出せないとか、メソッドの順番に依存しているクラスとかがありますよね。TDD のテストコード自体、オブジェクト指向設計に基づいているので、クラス同時は疎結合でないとうまくテストができません。複雑に絡み合ったクラスや機能の場合、長い手順を踏まないと呼び出せない UI みたいなものを自動化するのは大変な努力が要ります(もちろん、その努力に見合う、対費用効果があれば取り入れるでしょう)。そういう心理的な効果、行動経済学な考えからして、使いづらいライブラリを使いづらいまま作るのは、利用者のデメリットなんですよ。Microsoft のドックフードシステムってのもありますが、それのテストコード版です(にしても、.NET Framework で多々使いづらいクラス設計があるのは内緒です)。まずは、利用者の視点になってクラスを作ってみましょう。色々なパターンもあるでしょうが、あれこれとクラスを組み合わせ作る悪癖はやめましょう。それは、効率的なテストコードを作れなくする第一歩です。
利用者視点から作られたクラスは、そのままヘルプになります。テストコードがあれば、大抵のサンプルコードが要らないんですよ。逆に言えば、サンプルコードが簡単にかけなければ、テストコードも書けません。それほど複雑なもの(外部要因とか接続タイミングなどがあって、簡単にテストコードが書けないものは多々あります)の場合は、TDD に向きません。ちまちまとテストをしてください。また、テストコード自体をユースケースとして作ることも可能です。うまくクラス設計をやれば、UI なしでユースケースをテストコード内に埋め込むことができます。当然、うまくないクラス設計の場合は、複雑怪奇になって破たんします。クラス設計のリトマス試験紙にもなるのです。
複雑なモックアップを作らないと動かないテストコード自体も、避けるのがベターです。私自身もいろいろなプロジェクト単位で色々なモックアップを作ってきましたが、結局のところ、本番の DB 構造を使ったほうが便利だったり、試験環境を作ってガンガン廻したりするするほうが、複雑怪奇なモックアップを作るよりも作業効率がよいです。目標は「製品を品質よくしあげること」なのですから、まずはそれを第一に考えます。
そんな訳で、テストコードがガンガン減ります。以前は、「元コードの3倍ぐらいないと、テストの品質は保てない」と公言していましたが、訂正します。テストコードは1行だけでも ok です。ユーザー視点のテストコードが少しだけあれば、それに合わせるようにプログラムを品質よく上げる環境が、現在では揃っています。インテリセンスを効かせたり、自動生成コードを使ったり、手作業の部分を減らしていけば確実にテストコードも少なく済みます。
最初にテストコードを書く
自前の ExDoc ライブラリには実にチープなテストコードしかついていません。実は、元ネタは 2年ぐらい前に作って、最近ふたたび機能を追加し始めました。巷の OSS なコードには大量なテストコードがあるのが普通なのですが、私の場合は面倒…じゃなくて作業効率を考えて少しだけのテストコードを書いています。
public void TestLoad() { var xml = "<root><person name='tomoaki'>masuda</person></root>"; var doc = ExDocument.LoadXml(xml); Assert.AreEqual("root", doc.Root.Name); // seek element ExElement el = doc * "person"; Assert.AreEqual("person", el.Name ); Assert.AreEqual("masuda", el.Value); string val = el % "name"; Assert.AreEqual("tomoaki", val); }
このテストコードを満たすように、ライブラリを作っていきます。結局、何をやりたいのかを先に決めておかないと、あれこれと無駄なクラスやメソッドばかりが増えていきます。このあたりは TOC(制約理論)の精神で、目標を決めて行動範囲を明確にする、っていう枠組みです。これが動作するためのコード自体はあれこと1000行弱ぐらいあるのですが、それぞれのテストコードは書いていません。必要があればちまちまと書いてもいいのですが、自己満足にしかならないので書きません。
逆に言えば、このひとつのテストコードが ExDoc というライブラリの動作を決定しています。それ以上でも以下でもありません。このテストコードを破壊するようなコードは ExDoc にもありませんし、破壊するようなコードを追加するときにはテストコードも死に、ExDoc の本体も死にます。それは別のクラスライブラリとして再出発したほうがいいですよね。
そんな訳で、オブジェクト指向設計をしている限りは TDD は有効だし、MDA のような別の手段が出てくるまでは TDD のほうが効率的です。勿論、WEB サイトを作るような時には不便だし、そこには踏み込めません。だって、WEB サイト自体はオブジェクト指向設計ではないのですから、もっと別なアプローチが必要でしょう? アプローチとしては、MVVM とか MVC とかテストしやすいように設計を組み立てることが必須です。テストしにくいものを無理矢理 TDD する必要はないし、TDD しても効果が上がらなそうなものに対してテストコードを大量に書くには、プログラマの人生は短すぎますからッ!!!
増田さん、前進してますね。
xUnit 自体が XP/アジャイルのプラクティスなので、プラクティス自体もアジャイル的にという感じです。と言いますか、TDD is dead やもともとの「テスト駆動開発入門」を見直すと、「硬直化しないように進める」ような指針はあるんです(後解釈かもしれませんが)。
まあ、どちらにしろ結果を出すことを優先にして原理主義に陥らないように進むのが適切かと思って「撃ちながら前進」なのです :)