テスト駆動開発とは何か、それを気に入っているのは何故か、あなたも使うべきなのは何故か

ペースが速い現代のソフトウェア開発環境では、テスト駆動開発(TDD)という言葉をよく聞きます。その利点だけでなく欠点についてもソフトウェア開発コミュニティでよく議論されています。TDDについて、”自己嫌悪に陥って屈辱を味わっている者に対する非現実的で効果のない道徳教育のようなものだ”と言う人もいれば[1]、”リファクタリングを使って迅速な設計を支援するただのツールだ”と言う人もいます[2]

「ダメなプログラマは全てに答えを持つが、優れたテスタは全てに疑問を持つ」

Gil Zilberfeld

しかし、TDDは新たな手法というわけではありません。広く知られている最も古い文献は1957年に出版されたD.D. McCracken著の『Digital Computer Programming: The First General Introduction in Book Form, Stressing Actual Work with Computers』です。TDDはその後の数年は広く採用されることはありませんでしたが、IBMが1960年代に行ったNASAのプロジェクトでは、開発者が”早くからユニットテストをMicro-increments(イテレーションごとの小単位の進捗)ごとに書いていました[3]

現代のTDD

1989年のある暑い夏の夜、Kent Beckは最初のTDDフレームワークとして知られているSUnitを開発し、Smalltalk上で試してみました。TDDはその時に生まれ、もっと正確に言うと、再発見されました。その後すぐに、アジャイルとTDDを推進する動きに促されてプログラマは自動化されたユニットテストを書き始め、こうしてテストが開発の領域に加えられました。最初は、多くの新しいものと同じように、”本業にかかる時間を増やす“だけのように見えました。私たちの多くは、そのようなやり方に従う利点を理解しておらず、TDDに対して懐疑的で、批判的に見ていました。時間と労力の無駄だと思っていたからです。

しかし、更に進む前に、一歩離れて考えてみましょう。TDDとは何でしょうか?

簡単に言うと、TDDは機能を記述する前に自動テストを書く行為です。例えば、Bobが素晴らしい新たなソーシャルネットワークのアイデアのために新しい機能を開発する必要があるとしましょう。Bobは自動テストを書くことから始めますが、機能を正しく実装せずに、テストを失敗(赤色表示)させます。次に、Bobはテストを成功(緑色表示)させるための最小限のコードを記述します。緑色表示になったら、Bobはコードをきれいにし、テストを実行して機能が正しく実装されたままであることを確認します(コードのリファクタリング)。重複することなく、コードと自動テストは他の人も簡単に保守できます。

このプロシージャは、”赤/緑/リファクタリングというマントラ”としてもよく知られています。赤はテスト失敗で、緑はテスト成功を指します。アジャイル手法を推進する専門家の1人である(Uncle Bobとして有名な)Robert Cecil Martinは、著書『Clean Code』の中で、本番コードを書く前にテストを書くというルールは、TDDの氷山の一角にすぎないと述べています。

「期限に間に合わせるための唯一の方法(仕事を速く終わらせる唯一の方法)は、常にコードをできる限りクリーンにしておくことだ」

Uncle Bob,
『Clean Code』

TDDを使うことの利点

TDDの本当の利点を知ってもらうために、1つ例を挙げて考えてみましょう。

TDDを使わずに新しいソフトウェアアプリケーションの開発を始めることを想像してください。私たちが実装するのは、ユーザがブログを管理するためのアプリだとしましょう。

早速、1つ目の機能を実装します。まずは、ユーザがブログを作成するために、ユーザ登録をする機能です。コードが完成したら、全てがスムーズに動作するかを手動で確認しなければいけません。最初の機能がうまく実装できたら、2つ目の機能のコーディングを始めます。ユーザがブログを投稿するための機能です。これが完了したら、その機能が正しく動作するかを手動で確認していきます。そして、それもうまくできていたら次のコーディングへと進みますよね。

しかし、新しく記述したコードがユーザ登録の処理(最初に実装した機能)に悪影響を与えなかったかどうか、どのように判断したらいいのでしょう? 最初に実装した機能についても手動でテストをすれば、変わらず動作しているかを確認できます。ということは、新しい機能を実装する度に、その機能に加えて、それまでに実装した機能を全てテストしなければいけないということでしょうか? そう、その通りなのです。TDDを利用しないプロジェクトではリグレッションが生じやすくなるため、新しいバージョンをリリースする度に全ての機能を手動でテストすることとなって、恐ろしくコストがかさみます。リグレッションとは、ソフトウェア開発の用語で、旧バージョンで正常に動作していた機能に、後続のバージョンでバグが発生することをいいます。

結果として、自動テストを利用するTDDを採用したプロジェクトでは、そうでないプロジェクトよりも、通常はバグが少なくなります。しかし、自動テストは無料ではありません。自動テストを利用すると、各機能の開発に20~50パーセントのコストが上乗せされ、割高になると感じるでしょう。しかし、ソフトウェアの開発が進み複雑さを増すと、1つ1つの機能を手動でテストするのは難しくなっていきます。いくつかの機能を実装した時点で、自動テストを書くために割いた時間は取り戻せるでしょう。

現在のソフトウェア開発コミュニティでは、自動テストツールを使ったプロジェクトの方が、そうでないプロジェクトよりもバグが少ないというのが通説です。更に、テスト用のコードを記述するために発生するオーバーヘッドは、本番環境でのバグが防げることを考えれば帳消しになる、というのも一般的な考え方です。バグが少なければ少ないほど、プロダクトの質は高くなり、ユーザの満足度は向上します。そして、それが開発者にとってもうれしいことであるのは言うまでもありません。なぜなら、開発者はバグの修正ではなく、より良いプロダクトを作ることに注力できるようになるからです。

テストはどのくらい必要か?

ところで、どのくらいのテストを行えば十分なのでしょうか? それぞれの機能ごとに、いちいち自動テストを書く必要はあるのでしょうか? それは”状況によります”。TDDでは、正式なテスト計画というものはなく、それぞれのコードに応じたテストを実施します。そのため、テストすべきコードがどのくらいあるのかということが、大きな議論となってくるのです。

私たちImaginaryCloudの経験からすると、コードベースの80パーセントをテストするのが確実なアプローチです。TDDを適用して数件のWebやモバイルのプロジェクトを達成した結果、テストカバレッジが80パーセントのコードは、それより低いテストカバレッジのコードよりも、大抵は高い品質になることが分かりました。

「”どうテストを行うか?”という問いには、一般的に答えられない。しかし”いつテストを行うか?”という問いには、一般的に答えられる。できる限り早めに、そして頻繁にテストを行うべきだ」。

Bjarne Stroustrup,
『The C++ Programming Language』

また、違う角度から見てみると、コードの80パーセント以上のテストカバレッジを実現しても、大きな利点はないことが分かりました。この80パーセントの法則を適用すると、バグはほとんど存在しなくなり、たとえそれ以上のテストを行ってもほとんど効果がなかったのです。

では、実際の開発での例を挙げてみましょう。最近のことですが、大がかりなプラットフォームを構築するプロジェクトにおいて、2つのスプリントの期限が差し迫っていたため、私たちは自動テストのカバレッジを60パーセント程度まで減らさざるを得ませんでした。その結果、60パーセントしかテストを行わないまま新たな機能がリリースされ、リグレッション(つまり、新しく機能を実装することで、今までバグがなかった機能に新たなバグが出ること)が多く発生してしまったのです。結局、2つのスプリントを60パーセントのテストカバレッジで終わらせたあと、私たちはバグ修正のためだけに次のスプリント全体を実施することを決め、テストカバレッジを80パーセントに戻すことにしました。もしテストカバレッジを下げなければ、このプロジェクトの遅延はスプリントの半分程度で済んだでしょう。しかし、カバレッジを落としたがために、1スプリント分の遅延が発生してしまいました。つまり、最初の段階でテストカバレッジを上げておけば、私たちは丸1週間を無駄にしなくて済んだのです。更なる機能を追加したり改善したりするのではなく、単にやってしまったことの尻ぬぐいをする人の気持ちを想像してみてください。

まとめ

私たちの経験則からすると、どんなに複雑な機能に対しても、自動テストは常に適用できます。テストカバレッジが80パーセントに満たない場合は、単純な機能に対して自動テストを書くだけです。そうすることで、バグの修正に時間をかけることはなくなりますし、バグが残るプロダクトをエンドユーザに使わせてしまうことも、ほとんどなくなります。

まとめると、私たちImaginaryCloudは、TDDを気に入っており、80パーセントのテストカバレッジは、私たちにとって特別な意味を持つ基準です。機能が複雑になればなるほど、テスト工程において80%のテストカバレッジを実現することの優先順位が高くなっていくのです。