デブサミ関西2012での講演内容まとめ
はじめに
今月、GOOS日本語版が発売されました。
継続的デリバリーに続き、高木さんと一緒にお仕事をするのはこれで二冊目です。今回も多くの人に助けられて、目標としていた
デブサミ関西での出版にこぎつけることができました。関係者の皆さま、どうもありがとうございました。
講演では触れませんでしたが、ここで「実践テスト駆動開発」というタイトルの由来について少し書いておきます。原書のタイトルはご存じの通り、"Growing Object-Oriented Software, Guided by Tests"で、前半の頭文字をとってGOOSと呼ばれています。"grow"も"guide"も本書にとって外すことのできないキーワードでしたので*1、タイトルから外すことは考えられませんでした。ただ、これをうまく収めるタイトルがどうしても浮かばなかったため、直訳を副題としました。主題に今の名前を付けたのは、ケント・ベックの著作(Test Driven Development: By Example (Addison-Wesley Signature Series (Beck)))以来、TDDにとっては二冊目のバイブルという位置づけを大切にしようと考えたのです。
講演のタイトルを「テスト駆動開発の進化」としたのは、2002年のケント・ベックの著作と比べてGOOS本はどういう所が変わっているのか、という点にフォーカスしているためです。
さて、今回の講演のアジェンダです。
- テスト駆動開発の進化とは?
- GOOSのポイント
- 進化という観点だけではとらえきれないGOOSのポイントについてご紹介します。
- 開発の現場で活かすには?
- GOOSで紹介されるテスト駆動開発を実際のソフトウェア開発のライフサイクルに組み込むにはどうすればいいかを考えます。
それでは内容に入っていきましょう*2。当日使ったスライドはこちらです。
そもそもテスト駆動開発とは、プロダクションコードに先立ってテストコードを書く(テストファースト)ことで、コードを動かしながら完成に近づけていくという手法です。まず失敗するテストを書き(レッド)、そのテストを通るようにし(グリーン)、先に進む前にリファクタリングする、というリズムは、黄金の回転と呼ばれたりもします(詳しくは(こちら)。
具体的にFizzBuzz*3を使って例を示します。TDDでは、プロダクションコードを書く前に、まずはテストコードを書きます*4。
@Test
public void when1then1() throws Exception {
FizzBuzz ex = new FizzBuzz();
String result = ex.result(1);
assertEquals("1", result);
}
シンプルなテストですが、これだけでも多くの判断をしています。
- クラス名は
FizzBuzz
にしよう
- メソッド名は
result
にしよう(これでいいの?)
- 戻り値は文字列にしよう("fizz"と"buzz"があるので)
この状態では実コードがありませんのでコンパイルも通りません。コンパイルを通しながら、最もシンプルな仮実装をするとこうなります。
public class FizzBuzz {
public String result(int i) {
return "1";
}
}
かならずしもreturn "1";
である必要ななく、return Integer.toString(i);
でも構いません。どこまで先に進んでいいかということについてTDDでは特に何も定めていないのです。重要なのは、「何をテストすべきか」という自分の理解を深めながら先に進むことです。個人的には、まずはできる限りシンプルなテストと実装(この場合であれば「1を渡したら"1"が戻る」)でインターフェイスを定め、その後で仕様に関するテストを計画的に書いていくのが良いのではないかと思っています。
さて、テストがグリーンになったところでリファクタリングです。result
というメソッド名がなんとなく気になりますが、今は先に進みましょうか。またテストを書いてレッドにします。インターフェイスが定まったので、次は仕様のテストですね。今回は、数字のテストを置いておいて先に"fizz"をやりました。
@Test
public void 引数が3だったらFizz() throws Exception {
FizzBuzz fizzBuzz = new FizzBuzz();
assertEquals("fizz", fizzBuzz.result(3));
}
これを通すためにコードはこうなります。
public String result(int i) {
if(i % 3 == 0) {
return "fizz";
}
return "1";
}
これでふたつめもグリーンです。中身もとりあえず良さそうですね。こうして、5の倍数だったら、公倍数だったら、数字だったら、とテストを書いていくと、動かしながら完成品が出来上がります。テストがあるのでリファクタリングを安心してできるところも、TDDのいいところですね。
ここまで読んで頂いておわかりの通り、TDDは何かひとつのクラスを実装するには非常に優れた手法です。しかし、普段我々が携わっているようなアプリケーション(私の場合はWebアプリケーションになります)では、どこから手を付けて良いのかわからなくなってしまいます。こうした疑問に対して、スティーブとナットはやはりテスト駆動開発の黄金律を使って立ち向かいます。その黄金律とはすなわち、まずは失敗するテストを書くというものです。ただし、そこで彼らが最初に書くテストは、「受け入れテスト」です。受け入れテストでテストするのは、システムが実装すべきフィーチャ、顧客に対して提供すべき価値ということになります。そしてこの受け入れテストはエンドツーエンドに実施しなければいけません。エンドツーエンドとは、システムの「端から端まで」、たとえば、ユーザーインターフェイスからデータベースまでということです。
この受け入れテストから書き始めるTDDは次に示すような二重のループを描くことになります。
外側のテストは実装すべき機能がどこまでできているかを示す進捗の指標となり、内側のテストによってコードの質が高く保たれます。この外側のループと内側のループを繰り返し辿ることで、ソフトウェアが作られていきます。本講演のタイトルにしたテスト駆動開発の進化とはすなわち、コードを書くことから、ソフトウェアを作ることへということなのです。
GOOSのポイント
さて、前述した二重のループに加え、GOOSにはいくつか特筆すべきポイントがあります。ここではそれを二つとりあげます。それが、モックとウォーキングスケルトンです。
モックについて
ケント・ベックがJUnitの作者であるように、スティーブとナットはjMockの作者です。jMockは二人のオブジェクト指向観をベースに作られています。彼らはオブジェクト指向システムを、「協力しあうオブジェクトの網の目」ととらえます。クラスの継承関係よりも、オブジェクトがどのようにコミュニケーションし合うかが大切だと考える彼らにとっては、クラスよりもインターフェイスが重要です。
さて、そのような発想に従い、網の目としてオブジェクトが構成されていると、テストをするのが難しくなってしまいます。あるオブジェクトをテストしようと思うと、隣り合う他のオブジェクトも動いてしまうことになるからです。
そこで登場するのがモックです。モックを使うと、隣り合う他のオブジェクトをニセモノと差し替え、テストの際に思い通りの動きをさせることができます。この話は具体的な方がわかりやすいので、具体例として「モックによるインターフェイスの発見 - Digital Romanticism」をご紹介しました。興味のある方はご一読くださいませ。なお、前のブログの方には書いていませんので、テストコードの表現がシーケンス図と同じになるという点をここで強調しておきたいと思います*5。
キーに対して値をロードして返すシーケンス図がこちら:
実装がこちらです
@Test
public void キャッシュされていないオブジェクトはロードする() throws Exception {
final ObjectLoader mockLoader = context.mock(ObjectLoader.class);
context.checking(new Expectations() {
{
oneOf(mockLoader).load("KEY");
will(returnValue("VALUE"));
}
});
TimedCache cache = new TimedCache(mockLoader);
assertThat((String) cache.lookup("KEY"), is("VALUE"));
}
例に示した仕様ではオブジェクトローダーが事前に存在することが想定されていましたが、実際にテストを設計しながらインターフェイスが発見されるということも十分に起こります。受け入れテストからはじめて、外側から内側へ、インターフェイスを発見しながら開発を進めていく点がモックを使ったTDDの特徴になります。モックを使ったテストコードを書くことは設計の活動ですので、テストファーストとはいえコードを書くことにこだわりすぎず、色々な図を使いながら考えて進めていくのが良いのではないかと思います。
そのようにインターフェイス(あるいはオブジェクト)を発見するためのパターンがGOOSには三つ出てきます。
- 分解(Breaking out)
- あるオブジェクトが責務を持ちすぎている場合、ふるまいの凝集した単位を分割するというものです。
- 発芽(Budding off)
- 新しい概念が登場したときにプレースホルダー型をラップする(値)もしくは、サービスを定義する(オブジェクト)というものです。分解は既にあるものを分けるのに対して、発芽は新しい概念が導入される点が特徴です。
- 包含(Bundling up)
- 関連するオブジェクトの集団をひとつのオブジェクトにまとめるというものです*6。上記二つは分けるパターンだったのに対して、これはまとめるパターンです。
これらのうち、分解と包含は基本的な考え方だと思いますが、発芽はきれいなコードが書けるようになるためのいいテクニックだと思います。
なお、もう一つ重要な点として、ビジネスロジックの中核は、ビジネスドメインの言語を使うべきだ、というものがあります。これについて詳しく知りたい方は、ぜひ以下の本をご覧ください。
ウォーキングスケルトンについて
GOOS流のTDDでは、フィーチャの開発とエンドツーエンドテスト基盤の開発を並行して進めなければなりません。しかしこれには、テストが失敗したときの原因がどちらにあるかわからないという点を始めとした難しさがあります。ウォーキングスケルトン(Walking Skeleton)とは、これを解決するための方法です*7。まずは、できる限り薄いフィーチャのスライスを取り出し、それをビルド/デプロイ/テストするための基盤を作ります。こうした基盤が一度できれば、あとは少しずつテストを増やし、プロダクションコードを育てていくことができるようになります。詳しくは以下の本をご覧ください。
中間のまとめ
これまでの内容をいったん整理します。まず第一に、GOOS流TDDの核心は受け入れテストとユニットテストにより構成される二重のループにあります。それを円滑にまわすために、まずは薄いスライス(ウォーキングスケルトン)を使ってテスト基盤を作り、フィーチャを増やしていくという形をとることになります。第二に、テスト駆動開発という名前がついてはいますが、ここで言われているテストはシステムのふるまいです。つまり、テストを考えるということは、システムを外側から見たときのふるまいを考えるということに他なりません。こうした考え方については、「BDDの導入 - Dan North - Digital Romanticism」をご参照ください。
開発の現場に活かすために
さて、ここまでGOOSに書かれたTDDの特徴を見てきました。自社システムの開発を数人で行っているチームであれば、ここまでのノウハウで十分にうまくいくと思います(もちろん、スクラムのようなある程度の開発プロセスに関する方法論は必要でしょうが)。しかし、プログラマ30名〜40名で行う受託開発を考えると、なかなかこの形に持ち込むのが難しそうです。最後に、こういった中規模〜大規模の開発現場でGOOSを活かすにはどうすればいいかを考えていきたいと思います。
二重のループをどう開始すればよいか、という問いは、二重のループの外側を見ることでしか解決できません。二重のループに入る前の段階として、GOOSには以下のステップが説明されています。
- 問題を理解する
- アーキテクチャを定める
- ビルド/デプロイ/テストを自動化する
ここで重要なのは1.と2.、特に2.のアーキテクチャです。GOOSが想定しているアーキテクチャは、タイトルにある通り、「オブジェクト指向ソフトウェア」です。しかし、ここで想定しているような中規模〜大規模の開発では、すべてを「オブジェクト指向」で作ることはあまり現実的ではありません。まず重要なのは、業務分析を行った上でドメイン(ここでは「機能の大分類」と読み替えて頂いても構いません)を抽出し、それぞれのドメインごとに特性を分析することです。
ここで分析すべき特性とはすなわち、業務の複雑さをどこで受け止めるべきか、ということです。ここでは二つ分類を出してみます。
- エンティティ主体のドメイン
- ロール主体のドメイン
エンティティ主体のドメインは、普段多くの方が目にしているものだと思います。つまり、業務の複雑さがデータモデルに吸収され、E-R図とSQLがビジネスロジックの中核になるようなドメインです。このようなドメインであれば、ムリにオブジェクト指向的に作る必要はなく、SQLを気持ちよく書くためのSQLテンプレートエンジンを準備した上で、入力チェック、DBアクセス、編集処理といった手続きをトランザクションスクリプトとして実装することが正解となります。このようなドメインに対してO-Rマッパーを当てはめた結果、コード内に大量のJPQLが埋め込まれ結果として可読性が落ちてしまうケースを見たことがある方も多いのではないでしょうか。また、こうしたドメインの場合、テストとして設計するべきは、シナリオではなく、データのパターンになります。ユーザの操作だけで見ると「詳細画面を表示する」という一シナリオで済んでしまうところ、「このフラグがこうなっている場合はこの項目は表示しない」、あるいは「ここがこういう状態の場合にはリレーションのこっちから値を持ってくる」といったパターンが出てくることになります。
一方で、処理が複雑で、ロール間のインタラクションとしてモデル化した方が適切だと思われるドメインも中には存在します。例としては、決済処理や権限といったよくある共通処理、あるいは外貨金利の日またぎ計算といった特殊な業務ロジックが挙げられます。このようなドメインに対してトランザクションスクリプトで立ち向かってしまうと、処理が重複したり、メソッドが長くなりすぎたり、という問題が発生します。またテストの設計も、「こういう条件でこういう入力があったらこのインターフェイスを呼び出す」という形のシナリオになるでしょう。
ここでは二つの分類を出しましたがこれが二つである必然性はありません。各ドメインを記述するのに適切なパラダイムは他にもあり得ると思います。重要なのは、作るものの特性を見極め、それに合わせたアーキテクチャを策定しようということです。GOOSではソースコードは「育てる」ものだとされていますが、そのソースコードが育つ方向性を事前に規定するのがアーキテクチャなのです。