本エントリはTDD Advent Calendar jp: 2011の12/8の担当分の記事で、id:t-wadaさんの「右手に感情、左手に数値 - カバレッジを味方にしよう - t-wadaの日記」に続くものです。
はじめに
TDDはシンプルな原則に則った手法ですが、とっつきの悪さもしばしば持たれがちです。また一連のTDD Advent Calendarで起こった議論や会話の中でも、TDDの始め方はどうすれば良いかという話が散見されましたので、自分の担当枠では「TDDのはじめかた」についてまとめたいと思います。なお紹介するのはあくまで数ある入門方法のうちの1つです。たぶん他にも色々な良い入門方法があると思います。
全体像
紹介する入門方法は以下のようなステップバイステップの構造となります。
- いつでも軽快に使えるユニットテスト環境を構築する
- 必要と感じたらすぐテストを活用する
- テスト並行を目指す
- 継続的テスティングを目指す
これは全て完了しないとTDDを導入できないわけではありません。ステップを進めれば進めるほど、TDDがやりやすくなる感じです。もちろんステップの途中からTDDを始めても問題ありません。
なおこうした事前ステップをなぜ必要とするのかについてですが、これには2つ理由があります。
- まずTDDの前提スキルを無理なく身につけるためです。TDDでは製品コードとテストコードをすばやく切り替えながら開発を進めるため、軽快にユニットテストを書ける環境や技能が特に重要です。そうしたものを事前準備できていれば、TDDを無理なく実践できるようになります。
- またTDDのメリットをより活かすためです。「 TDDを学ぶべき10の理由 #TddAdventJp - やさしいデスマーチ」の通り、TDDは様々なメリットを持っていますが、それらはTDDの外にも良い効果をもたらします。広範なユニットテストの活用環境を導入しておくと、それらメリットをより活用できるようになるため、TDD導入の費用対効果が改善します。
ではそれぞれのステップを順々に説明していきます。
準備
1.いつでも軽快に使えるユニットテスト環境を構築する
まず最初に、思いついたらすぐテストを書いて実行できるユニットテストの環境を構築しましょう。
これはTDDと親和性のあるユニットテストのテスティングフレームワークの導入で実現できます。なお「TDDと親和性あるテスティングフレームワーク」とは、具体的には以下のような特徴を持つものが該当します。
- 軽快にテストを実行できる。TDDにとって、実行に1秒以上かかるテストはもう遅すぎます。そのような実害ある遅さの直接的原因になるフレームワークは避けた方が無難です。
- 簡潔なテストコードでテストを実装・実行できる。Assertion MethodやTest Methodなどの構文が簡潔で、またテストを実行するためのコードもコンパクトに済ませられるものが推奨されます。例えばテストディスカバリ機能(テストメソッドを書いたら自動的にそれをピックアップして実行してくれる機能)はあると望ましい機能の1つです。
- 明快かつ軽快にテスト結果を表示できる。テスト結果を通知するUIは、REDかGREENかの把握を一瞬で済ませられるように、色や配置等が工夫されていると効率が向上します。
- 製品コードの開発環境と連携可能。TDDでは製品コードとテストコードの開発を頻繁に切り替えていくことになりますので、それらの編集領域を簡単に切り替えられるような環境が望まれます。例えばIDEに組み込み可能なフレームワークは有望な選択肢です。
なおTDDと親和性の高い定番のテスティングフレームワークも存在します。例えばEclipse×JavaならJUnit、Visual Studio×C#ならNUnitが主流です。また言語ごとではC++ならgoogle testが、RubyならRSpecなどが有望でしょう。そうしたものを開発環境に導入すれば、無難に環境を構築できます。
ウォームアップ
2.最初のステップ:必要と感じたらすぐテストを活用する
環境を構築できたら、まず「必要に感じたらすぐにユニットテストを書いて実行する習慣」の実践を始めてみましょう。具体的には以下を日々の開発に取り入れてみてください。
- バグがありそうなコードを書いたら、とりあえずバグがないことを確認できるまでユニットテストを書く。
- リファクタリングを行うときは、コードの動きが変わらないことを確認するテスト(回帰テスト)としてあらかじめユニットテストを書く。それでリファクタリング前後でデグレードが起こっていないことを確認する。
- よくわからないコードがあれば、とりあえずユニットテストを書く。テストを継ぎ足していくことで、そのコードがどのような動きをするか、きちんと動いているか確認する。
- バグが出たら、とりあえずユニットテストを見直す。テスト上でバグを再現できないか調べたり、バグの原因をテストで絞り込んだりする。
なおここでのテストは使い捨てでも構いません。とにかく不安なコードやリスクある作業に直面したら、ピンポイントでテストを書いて即時にリスクや問題を軽減していきます。またテストの網羅度も、慣れないうちは主観的な判断で調整してしまって大丈夫です。例えば不安がなくなるまでテストを書くというのはスタートアップ段階では考え方としてありでしょう。
この習慣の実践では、対価として「テストを書く労力」と「最低限のテスタビリティを確保する労力」が要求されます。
ただその労力と比べて、この習慣で得られるメリットははるかに大きいです。追加・変更・解析・デバッグといった諸々のプログラミング作業の中で、バグやミスを防止できるようになります。また不安やリスクを早期解消できるので、より良い精神状態でプログラミングを進められるようになるでしょう。TDDの有無に関わらず、身につけておいて損はないスタイルといえます。
3.テスト並行を目指す
「必要と感じたらすぐテストを活用する」習慣を実践できるようになったら、テストが必要と感じる閾値を下げて、より多くテストを書くようにしましょう。具体的には、「明確な必要性に応じてテストを書く」から「少しでも必要性を感じれば予防的にテストを書く」(例えば「もしかしたら問題があるかもしれないからテストを書く」「将来的に問題がでそうだからテストを書く」)へ考え方を転換していきます。
こうした考え方の転換のゴールはテスト並行(Test-Concurrent)です。テスト並行は、テストを書くタイミングが後になろうと先になろうと、とりあえず製品コードと並行させてテストを書いていく考え方です。さらに生み出したテストは、製品コードと一心同体のものとして、後の変更やリファクタリングでも再利用していきます。
具体的には、とにかく可能な範囲で以下を実践していきます。
- 製品コードを追加する際は、きちんと追加できたかユニットテストで確認します。
- 製品コードを変更する際は、変更で変えたいふるまいと、変えたくないふるまいそれぞれにテストを書きます。変えたいふるまいに対しては、コードを追加する際のテストできちんと変更が実現できているか確認します。変えたくないふるまいには回帰テストでデグレードが発生していないか確認します。
- 既存のテストに問題があれば、テスト設計やテストコードの再構築を後付けで行います。
- 製品コードに変更が発生した際は、妥当であればテストもそれに同期させて更新します。
こうした習慣を続けると、以下のようなメリットが増大していきます。
- テストの作業ミス・バグの予防効果を継続的に享受できます。
- テストをペアとしてもつ製品コードが増えています。それら既存のテストの再利用により、変更やリファクタリング作業をより少ない労力で実施できるようになります。
ただ一方で、継続的にテストを書いていくと、単純にテストを書く労力が増える以外に、3つ問題がデメリットして浮上します。
- まずテストを継続的に再利用していくに伴って、テストを保守する労力が増加します。たとえば製品コードのインターフェースが頻繁に変更されると、それにあわせてテストコードも頻繁に変更しなければならなくなります。
- 次に、テスト設計の整合性がより強く要求されます。網羅性が曖昧で何をテストしているかわからないテストコードは、再利用時にバグを見逃すリスクを増加させます。またテスト設計が不適切なままテストの網羅性を上げようとすると、しばしば乱雑なテストの山ができ、テストの保守をより困難にします。
- 最後に、テスタビリティが広く求められます。特にテスタビリティが粗悪なレガシーコード相手を相手にした場合、テスト書けるようにするための修正で大変な労力と精神力を消費することになります。
こうしたデメリットは、悪化するとユニットテストの導入意義にもかかわりますので軽減を図っていかなければなりません。以下のような対策が必要となります。
- テストの保守性を作りこむ。例えば保守しやすいようにテストを書く(重複を避ける、よい名前を使う、小さな単位に分割する)、変更の影響が限定されるように堅牢なインターフェースを製品コードで確保する、等。これに関しては以下の書籍にid:t-wadaさんが書かれた良い記事があります。
- テストコードの実装プロセスを工夫する。例えば詳細なユニットテストは落ち着いてから実装し、それより以前のテストはピンポイントで必要に応じて書き捨てていくなど。これについては拙著ですが以下で一部触れさせて頂いています。
- より上位の設計からのテスタビリティの作りこむ。変更が発生しても影響範囲が限定されるアーキテクチャを構築するなど。これに関しては以下に良い示唆が含まれています。
- 適切やテスト分析、テスト設計の手法にのっとってテストを設計する。例えばコードカバレッジを評価する、同値分割やドメイン分析、デシジョンテーブルなどで仕様を整理するなど。適切なテスト設計手法に基づいてテストを作っていけば、コンパクトかつ網羅的なテストが得られます。これに関しては以下の書籍が参考になります。
なおユニットテストの習熟が進めば、上記メリットは大きく、デメリットは小さくなってより広範囲にテストの恩恵を受けられるようになります。うまくメリットとデメリットのバランスをとりながら、継続的にデメリットを下げる対策をしていきましょう。テスト並行はそれをやるほどの価値を持っています。
(応用のステップ)4. 継続的テスティングを目指す
このステップはTDD入門としては、必要性はそんなにありません。効果はあるのもの、TDDと離れた作業の比率が増えるためです。
テスト並行を広い範囲で適用できるようになったら、作ったテストを頻繁に自動実行する継続的なテスティングを目指していくのがひとつの手です。これはJenkinsといったCIツールを導入し、そこにテストの実行環境を組み込むことで実現できます。
今回は詳細は割愛しますが、継続的なテスティングを実現すると、テストを実行可能な状態に、またテストをGREEN状態に維持するためのサポートを自動化できるため、テストの保守が容易になります。結果、ユニットテストをどこでも・いつでも活用できる状態を確保しやすくなり、製品コードの変更・保守が格段にやりやすくなります。
ただ継続的なテスティングの実践ではテストの保守性がより強く要求されるため、「テスト並行を目指す」で書いたデメリット軽減策をさらに推進していく必要があります。
TDDの導入
TDDをはじめる
さてTDDですが、少なくとも「必要と感じたらすぐテストを活用する」習慣を実践できていれば導入の準備はできています。また「テスト並行」がある程度できていれば、それに比してTDDを活用できる範囲も大きくなっているはずです。すでにTDDを解説する世の中のウェブや書籍で、スムーズにTDDを習得できるようになっていると思います。
なお参考資料としては、とりあえずこだわりがなければ以下の本が定番です。本エントリで長々と解説するよりこの本を読んで頂いた方が色々無難かと思います。
なお本格的な解説は上記書籍にお任せするとして、残りはとっつきの悪さや誤解が少なくないテストファーストの導入について、補足資料として簡単に解説します。
テストファースト
最初の単純な考え方の1つとして、テストファーストは作業を細切れにする手法と考えると混乱が少ないと思います。
例として、ピンポイントのテストであれテスト並行であれ、テストが必要そうなコードを新規に書く機会があったら以下を実践してみましょう。
- 書こうとしている製品コードの仕様を細切れにして(前述の書籍「テスト駆動開発入門」ではこれをリストアップしたものをテストリストと呼びます)、小さな仕様をピックアップします。
- ピックアップした小さな仕様についてテストを書いて失敗させます。
- 製品コードを書いてREDをGREENに変えます。
テストの大きさは、なるべく一息でGREENにできるような細かい粒度にするのが無難です。例えば「4で割り切れる年は閏年、100で割り切れる年は平年、400で割り切れる年は閏年」と判定するうるう年判定関数を実装する場合は、以下のようになります。
- 「西暦年が4で割り切れる年は閏年」と小さな仕様をピックアップして、
- 「4で割り切れる年は閏年と判定する」ことを確認するテストを書いて失敗させ、
- テストを通す最小のコードを書いてテストをGREENにします。
こうした流れでは、最初のREDで次に進める作業範囲を明確化して、次にRED→GREENで作業が完了していることを明確化しています。これにより自分の作業に対する直感的かつ即時のフィードバックと、作業の自己統制感、そしてきちんと作業をこなしたという安心感を確保します。またテストを使ったことによる副産物として、それなりのテスタビリティと、テストコードを確保します。
なお前述「テスト駆動開発入門」では、「Fake It」とリファクタリングというアクティビティが解説されていますが、これは作業の粒度調整手段ともいえます。例えばFake Itは「インターフェースを実装する作業」と「内部実装を行う作業」を細かく分離する手段として使われます、またリファクタリングも、「必要な機能を実現する作業」と「完成されたコードを実現する作業」を細かく分離する手段として有効です。
最後にここで書くテストはあくまで作業確認のためのテストです。作業単位で累積していくだけですので、網羅性や全体整合が取れない場合もしばしばあります。網羅的なバグ出しなど、他の用途でテストを再利用したいならば、作業の目途がたったところで別途テストを再整理する必要があります。
設計のためのテスト
テストファーストができるようになったら、今度はテストに設計やふるまいを洗練させる視点も取り入れてみましょう。流儀は複数ありますが、今回は製品コードの使い勝手にフォーカスした例を以下に示します。
- テストコード上で実際に製品コードのインターフェースや呼び出し方法を書いてみます。
- 製品コードのインターフェースや呼び出し方法について、どのようなものが良いか考えます。
- たとえば「コンストラクタで値を渡すか」「setterで値を渡すか」「static関数にするか」といった検討を行います。
- 他のclassなどが必要になったら、MockやFake Itといった仮実装を活用します。
- テストを実行してRED状態にし、TDDにより製品コードを実装します。
こうした内部実装に入る前に一呼吸おいてインターフェースや呼び出し方法を考えるアプローチは、製品コードのテスタビリティや堅牢性、使いやすさを向上させる点で大変有効です。なおこれをより上位の呼び出し元で実践したものはOutside-In TDDと呼ばれます。
経験を積む
なおTDDは原理主義的に実践しなくても大丈夫です。あくまでテスト並行をベースに以下のようなフローをぐるぐる進めていきましょう。
- テストが書けないなら...
- 製品コードのテスタビリティを改善する
- ユニットテストをあきらめて上位のテストで代替する
- テストが書けるなら...
- バグのリスクや分かりにくさを感じたら...
- テストを継ぎ足してリスクや問題を軽減する
- テストに問題があれば...
- 追加でテスト設計やテストコードの再整理を行う
とにかく呼吸するようにユニットテストを活用していきましょう!それによりTDDが使える機会は増えますし、TDDの恩恵も倍増していきます。そしてそれを続けていけばさらにTDDの習熟が進んでTDDの費用対効果が上がり、TDDを活用できる機会が拡大します。
まとめ
長くなってしまいましたが、まとめると以下のようになります。
- ユニットテストを軽快に使いこなす習慣を実践していると、TDDを楽に導入できるようになります。
- TDDは一律に実践しなくても大丈夫です。呼吸するようにユニットテストを活用する中で、活用できる領域でガンガン実践していきましょう。
以上、TDD入門で詰まったときに本エントリが役立てば幸いです。
つぎは
明日の担当は id:yujiorama さんです!