TDD Boot Camp体験記

12/19に開催されたTDD Boot Campに参加してきました。
一言でイベントを総めると、本気で楽ませて頂きました。
また、前々から疑問に思っていることをレビュー中などに意見としてぶつけて、得る事ができたものがたくさんありますので、意見の整理と確認も兼ねまして、こちらにイベントの内容を綴らせて頂きます。
多くの方から意見を頂いたりしましたが、話の出所などを完全には覚えていないので、場合によってはさも自分の意見のように書かれているところがあると思います。特にTwitterのログに関しては大いに参考にさせて頂いています。その点に関しましてはあらかじめご了承下さい。
また、間違っている部分や気になる部分などがありましたら、意見をいただけると幸いです。
かなりの長文。注意。


まず前日から深夜バスを使って東京渋谷入り。その時間、バスがやや遅れたとしても6時。
最初の草書は24h営業のマクドで書いていますが、こういうときに本当に役立ちます。
渋谷駅前に出て、とりあえずハチ公を見て*1そこから開いているカフェを探す旅に。
気温が低かったので、早く見つけたかったのですが、携帯を使うと近くにマックがあることが分かり、そちらへ。
朝食をとりながら、持って来たリファクタリング・ウェットウェアを8時ぐらいまで読んでいました。*2
そして、昼食をコンビニで買った後、Twitterを見るとid:katzchangさんが近くに居る事が判明。合流することに。
合流の後、帰りが当日の新宿発の深夜バス*3ということも分かり、最後までご一緒させて頂きました。イベントが終わった後の会話については後ほど。


会場の青山オラクルセンターについて感じたのは、何というか会場がでかい。
13Fの会場から富士山がはっきりと見えましたよ。
その後、ustで顔見せ許可のところに着席。カメラがちょくちょく動いていたから、多分探せば映っているかも知れません。


10時になり、id:t-wadaさんの講演がスタート。
まず印象として受けたのが、すごいプレゼンテーションが上手だということでした。よいしょなどではなく。
プレゼンテーションZenに書かれている基本を押さえた上で、うまく魅せているという印象を受けつつ、重要なTDDの基礎を教授して頂きました。

  • 黄金の回転*4:Red→Green→Refactor→Red...の繰り返し。

コードが綺麗/汚いという軸と、正常に動作する/しないという軸の平面が存在するとき、Red(コードが汚い/正常に動作しない)から、Green(コードが汚い/正常に動作)に持って行き、それをRefactor(コードが綺麗/正常に動作)に引きあげる。その目的はコード、そしてその開発者が健康である(あり続ける)こと。

  • テストのテスト(Fake it!):Greenの明示的な実装

テストのテストを簡単に行えるタイミングが存在する。それは、一番最初の実装でテストに成功する固定値を返す実装をするとき。例えば、MyMath.add(1,2)というメソッドに対して、常に固定値の3を返すという実装を行うこと。
もちろん、この例では成功するが、明らかにaddとしての性質を満たす結果ではないので、一見無駄で必要のない実装のように思える。しかし、ここで「テスト:assertThat(MyMath.add(1,2), is(3))*5」に対して赤が出れば、テストがそもそもおかしいことが分かる。言い換えれば、このタイミングで「テストのテスト」が行えるというメリットがある。

  • 一気にたくさんやろうとしない:宮本武蔵がごとく

文のまま。人間一度に複数のことを同時にするのは難しいので、並列ではなく直列にして1つずつ取りかかる。

綺麗さを追求すると、ある意味ではどこまで言っても終わらない。1イテレーションの時間を短い物に区切って、テンポ良く素早く回して行くべし。後々コードがまずいことが判明したのなら、そのときに再びリファクタリングするべき。

  • TDDの真の目的は健康

先にも述べたとおり。テストがあるという安心感が人のストレスや恐怖をなくし、開発者自身も健康になります。

明白で安全な実装こそ、プロとしての意識であり、TDDはそれを体験できる。

  • TDDはスキルであり、習得が可能

何をどの粒度でどの程度の時間のサイクルで取り組んで行くか、それはTDDを実践していくことで身につけることのできるスキルである。その手法に慣れるために、写経(例えばBeckテスト駆動開発入門)をすることはすごく有用である。


これらをご講演頂いた後、FizzBuzz問題*6を実際にTDDを体験するということで、eclipse+Java+JUnit4環境で各人での開発を行いました。
Red→Greenという流れを、eclipseの自動エラーコード補間機能で補いつつプログラムを作成しました。
以下、FizzBuzzクラスがsay(int)というメソッドを持ち、このメソッドが引数に応じた文字列を返すものとします。
また、この時のテスト名として日本語を使いました。わかりやすさ重視ということで、テスト内容が分かりやすい日本語でもOKだろうという話です。
そして三角測量*7。3の倍数の時のテストはGreen(sayは常に"Fizz")を返すというテストを書いただけではプログラムを直さず、5の倍数の時のテストを実装してRedが出てから、これをGreenにするようにコードを改善します。
このとき、既にあるテストに書き加えていく形でも構いません。そして、テストに対してもリファクタリングを行っていくことが重要です。TDDでは、これらも積極的に行って行かなくてはなりません。
また同時に、素早くテンポ良く回すということを心がけていくので、特にテストの組み合わせについて網羅性を重要視する必要性はあまりありません*8。むしろ、自分の間違いやすい癖を見つけ、そこを重点的にカバーしていくなど、ある程度自分の経験に基づいた判断をしてもいいという話があったと思います。
そして、普通の数字(3と5のいずれの倍数でもない数字)の場合のテストを書くなどして、それらのテストがあることをよりどころにリファクタリングを行っていきました。途中ではあったのですが、確かここらでタイムアップ。


Lasseさんの講演は翻訳用スピーカーの数が少ないということで、スピーカーの使用をご遠慮したのですが、正直英語の聞き取りはまだまだですね……orz
プレゼンテーションを見つつ、纏めたポイントとしては

  • デザインのセンス:良い・悪いを判断できる力を培うこと。
  • リファクタリングテクニック:適切なリファクタリングが行えるか。
  • ツールの習熟:良いツールを十分に使いこなす。例えばショートカットキーなど。話には出てこなかったけれど、この辺りとしてはプロダクティブ・プログラマがかなり役に立つ本だと思います。

いくつか本が紹介されていましたが、レガシーコード改善ガイドはかなり話の中心に出てきましたね。あとはマーティン先生のリファクタリング本であったり、アジャイル奥義なども話に出てきました。
話の後半は実際にコードをリファクタリングする様子を見せていただきました。
最後に質問が出ましたが、テストのためにprivateをpublicに変更した場合どうするかという話で、リファクタリングを(おそらく複数サイクル)行い、再びprivateになるようにコードを改善する、という答えを頂きました。
まあ、Red→Green→Refactorのサイクルの実践ということを考えるとあたりまえの用に思えるのですが、レガシーコード改善ガイドではその辺りは(確か)明記していなかったと思うので、非常に参考になりました。


講演の後、協賛の皆様がたの宣伝をお聴きしつつ昼食に。
色々ともらってきたので、時間ができたらちょくちょく試して見たいなー、と。
ノートやペンなんかはありがたく使わせて頂きますが。


午後からは言語毎に分かれてチームわけ。
Java:C#:Ruby:Python(+xUnit初心者) = 4:2:3:1ぐらいの比率だったと思います。Pythonを選んだのは2人、だったかな?
自分が選んだのはJava。その中で6人程度のチームに分けた後に、Java内ではMacグループが出来てました。そしてMacグループから追い出されたWindow使いの自分。
ポジションペーパーを使って同じ卓のかたと挨拶をしつつ、ペアプログラミングの準備が整ったあとで、今日の課題その1が発表されました。

お題:LRU Cache
一定の数に達したら使われていない順に要素が削除されていくMap*9のような入れ物を作りたい。

このため、Last Recently Used(LRU) Cache を今回作成する。

キャッシュの最大サイズに達したときに、最も使われていないデータから順に消されるMapのような仕組みが欲しい。


*サイズが2のLRUCacheの場合
# 1つも使われていない場合は最初に追加したものから消える
lru.put("a", "dataA");
lru.put("b", "dataB");
lru.put("c", "dataC");
lru.get("a"); # => null

# getされたら使われたとみなす。
lru.put("a", "dataA");
lru.put("b", "dataB");
lru.get("a") # => "dataA"
lru.put("c", "dataC");
lru.get("b"); # => null

最初のペアプログラミング@htadaさんと組んで行いました。
色々話しつつペアプロをしていた訳ですが、自分のテストに対する粒度の荒さが露呈しましたね……
その辺りを色々と突っ込みを入れられながら開発出来たので、個人的にはすごく勉強になりました。
この実装にはジェネリクスを使わなかったわけで1.4世代の警告ありのものを作ってしまった訳ですが、今回は時間が無かったということで。実際に作ったコードについては、考察を入れつつ別の機会に公開したいと思います。
簡単に説明すると、今回はArrayListとHashMapの組み合わせでLRUCacheを作成しました。ArrayListの中にあるものがアクセスされた場合、その要素を抜き出し最後に入れるという処理をおこな……あ”、今1つバグに気づいた。公開時に修正しときます……
ペアプロの時間が終わり、コードレビューへ。
工夫点や、自分に無かった着眼点(特に自分にとってはテスト対象について顕著でした)、今後への改善点など多くの点が見えてきますね。特に多人数(Javaチームの2組10名弱)でレビューしていたので、色々と気づかされる点が多くありました。


ここで疑問だった点をぶつけて見ました。
まずは「シナリオテストをTDDの最初に書いてもいいのか」という点です。
これまでの方式では、小さい事からのボトムアップの形になります。しかし、私はTDDの利点の1つに「あらかじめ理想の使い方を試した上で、そのクラスを作る事ができる」、すなわちシナリオテストファーストとしてTDDを使うことにより、設計の質が向上することを挙げられると考えています。
結論としては「良い」という意見を頂きました。もちろん、そんな理想系が最初から通ることはあり得ないので、@Ignoreアノテーションをつけるなどして必要な時まで無効化しておく必要はあります。こういったクラス作成の「ゴール」を決めておくことは非常に有用です。
ただし、それがただ1つのゴールではありません。リファクタリングなどによって、ゴールが形を変えてしまうかも知れません。その時には、その変化を受け入れる必要があります。
もうひとつ、「テストは分かりやすく書く物であるが、同じような処理をメソッド化しても良いか」という疑問をぶつけて見ました。
ここでの同じような処理のメソッド化とは、上のLRUキャッシュの例であれば、複数のデータを纏めてput出来るような可変長引数をとるメソッドを作ってもよいのかという点です。
結論から言えば「メソッド化するべき」です。そもそも、テストに対してもリファクタリングを行う以上、このような結果になります。
ただし、そのメソッドは明瞭で単純で無ければなりません。また、コレクションなどの処理を行う場合にたまたまテストが成功していないか(例えば、ループを書いているが全くループせず空の要素を作ってしまい、その結果テストが成功する)ということに注意を払う必要があります。


ここまでで、自分が疑問に思っていたことをかなり解消できたので、感謝をしつつ2回目のペアプログラミングへ。
お題は「要求変更」。まさに、プログラマが「えっ?」と言ってしまうような仕様変更です。
要求変更を要求する和田さん、楽しそうでしたねw

【注:要約】

課題1:サイズ変更*10
顧客「LRUCacheが好評なんだが、その保持できるサイズを自由に変えられるようにして欲しいんだ。」
プログラマ「えっ?」

課題2:時間依存なデータ
顧客「LRUCacheが好評なんだが、意図を汲んでさ、あらかじめ決めておいた時間が経ったら勝手にアクセスされていないデータを消すような仕様にして欲しいんだ。」
プログラマ「えっ?」

課題3:マルチスレッド対応*11
顧客「LRUCacheが好評なんだが、マルチスレッドのプログラムで使いたいので改良してくれよ。」
プログラマ「えっ?」


ペアプログラミングの相手*12を変えて要求仕様の変更に立ち向い始めます。
プログラムは自分のPCを使用して書いていたので、その内容を確認しつつキャッシュサイズの変更を行うテストを書いて行きます。
課題1でのやっかいな所は、既に登録されているデータの個数よりも小さい保持サイズが指定された場合、その中のいくつかのデータを破棄しなければならないところです。
ここには着眼してテストを書いて行ったのですが、……サイズを大きくした場合、正しくデータが登録されるかのテストを忘れていました。やっぱり、テストしなければならない所の勘所がまだまだ甘いようです。
ここまでに書いたテストを合計すると10個ぐらいあったわけですが、……すごいリファクタリングがしやすい環境になっています。
ちょくちょくメソッド名を変えたり、メソッドの抽出をしたりしたわけなのですが、そのリファクタリングの正しさをテストがばっちりと保証してくれるという良いサイクルが出来ていることを実感しました。
一応、自分たちで考えたテストはパスしたので、課題1を実装したとして課題2へ。


まず、時間というものをテストするということで頭の中に警鐘が鳴り響きました。
レガシーコード改善ガイドにおける、「単体テスト」、すなわち早いテストをすることが難しいからです。
しかも、このLRUCacheの仕様上、数秒どころではなく数時間単位で消去時間を設定することも考えられます。
だからといって、テストに

@Test
public void 12時間後にデータが消えるかを確かめるテスト
    throws Exception {
  // ... 準備
  Thread.sleep(12*60*60*1000L); // 12時間待機
  // ... 消えているかを確かめる
}

とかを書いて、テストする気にはなりませんよね。
また、Thread.sleepを使用する場合はその精度も問題です。
たまたまうまく行く可能性はありますが、1ミリ秒待たせようとしても(おそらく)うまく行きません。


ではどうするのか。
頭の中で数分考えたのち、ブログで前にほとんど同じことを教えてもらったことがあった*13ことを思い出しました。
ここでの話もあり、フェイクでの実装を思いつきます。
ゴールが自分には見えたので、それを導き出すために、いきなりインタフェースに分離せずに、フェイクをテストしながら作成しました。
このオブジェクトは、生成されてから何ミリ秒経過したかを返すメソッドを持っていますが、それとは別に何ミリ秒経過したかを設定できるメソッドを持っています。
そしてそのオブジェクトが完成した後に、LRUCacheにFakeオブジェクトをコンストラクタで設定できるようにしました。
こうすると、FakeはLRUCache内で使われますが、テスト中にその時間を設定していくことができます。
ここまで来て、初めてフェイクのインタフェースの分離を行いました。
もちろん、テストがあるのでこのリファクタリングは自信を持って行えます。
そしてその後、Fakeの他に実時間を返す実装を作成し、ポリモーフィズムによる有用性まで導いたところでタイムアップとなりました。


そして再びコードレビューへ。
なんとも面白い事に、

(レビュー終了後)テスト用のメソッドを作って見るのはどうだろうか

テストメソッドのリファクタリングを行って、一部の処理を簡単にしてみたよ。時間のテストについては少し問題がありそうだなぁ。

自分たちの説明(時間に関するフェイクを使ったテストの実装)

という、まるで仕組んだかのような順番でレビューが行われていきました。
フェイク・モックなどの利用についてはテストの常套手段ということで使い時の話がいくつか出ました。
ネットワークやDB、特別な環境が必要な場合には、テストの手段としてこれらが有効です。
また、テスト可能にするために層を分離するという話も出たと思います。
あと、自分でコードを書いていて疑問だったのが、「リアルの環境(この場合は、実時間を返す実装)はテストできるの?」ということでした。
これは「無理」です。テストをしようと思うと、VMや環境のテストをしなくてはならないからです。
なので、これに関しては簡潔に書く必要があります。また、クリティカルな領域を極力小さくするべきです。
実は今回の実装では「最初に呼ばれてから、メソッドが呼び出されるまでに経過した時間を返す」というメソッドだったので、

return System.currentTimeMillis() - startTime;

という実装を行っていましたが、ここには引き算という処理が入ってしまいます。
場合によっては、これが大きな問題になるかもしれません。
そのため、こういったテストと分離した部分に関しては簡潔に書く方が望ましいでしょう。
今回はフェイクをコンストラクタで渡していましたが、Dependency Injection(DI)というタイプに分類される手法で、コンストラクタ・インジェクションと言うそうです。この辺りの名前は聞き覚えが無かったので、調べて見ようと思います。


また、話に出た中にどこのAssertで問題が起きたか分かりづらいという話題がありました。
これはAssertはエラーを吐くと、それ以降を実行してくれないためです。
テストメソッドのリファクタリングによってループを含む場合などはそれが分かりづらい典型でしょう。
そう言った場合は、エラーメッセージの工夫などをする必要があります。
その煩雑さを避けるため、メソッドにAssert1つのみという流派もあります。
自分が知る限りでは、Clean Codeにその話が薦め的な形で載っていたと記憶しています。
ただしその場合は、テストの命名が難しくなるという話もありました。


特別レビューとして、Pythonで書かれたコードとLasseさんのRubyで書かれたコードが全体に公開されました。
Pythonのdoctestは普通に面白そうだし、Python言語にも興味があるので触りたい……のですが、色々と*14誘惑多いですw
Lasseさんのテストコードは、本当に無駄がない。
テストのリファクタリングが行われていて、すごく短い行で1つのテストが書けるように工夫されていました。
一部、Rubyを勉強してないので、時間関連のテストに関しては少し分かりませんでしたが。
ここでは、フェイクを使わず時間をテスト時に操作することで(おそらく簡易的に)テストしていました。
あとで懇親会で聞きましたが、PerlRubyなどでは、こういった書き換えができるようです。
まあ、フェイクを使った方が安全ですが、大体1時間半ぐらいでテスト・実装を作られていた*15そうなので、あの簡易実装もありだなと思いました。


レビューが終わって、閉会式の流れに。
そこで賞の発表があり、id:t-wadaさんから時間テストのフェイクに関して評価して頂き、賞を頂きました。
まさかの、という感じだったので正直嬉しかったです。
このタイミングまで挨拶できていなかったので、このとき初めて挨拶しました(汗)。大変失礼致しました。


その後、懇親会。
主にJava卓だった方と交流して、今日の感想や今後のイベントの話などを聞かせて頂きました。
そしてみんなでテーブルなどを元の位置に戻す。
会場の日本オラクルを出たのが9時ぐらいでした。
その後、新宿まで行った後にid:katzchangさんと休憩兼食事を取りながら、今日のイベントの振り返りや疑問点などを話して11時ぐらいに分かれました。
深夜バスの発車まで40分あり、余裕があるようだった……のですが、まさか40分探してもバス停が見つからないという結果に。
交番で聞いても分からないと言われ、発車予定時刻を過ぎたので、あきらめようとしたときに運転手さんから電話の助けがあり、なんとかバス停にたどりつけました。


教訓:東京のバス停を甘く見るな。


そうして、トラブルも多少はありましたが、本気で充実した一日を過ごすことができました。
イベントを企画・開催して下さった方々、およびイベントに参加された皆様、本当に良いイベントでした。
ありがとうございました。

*1:お初にお目にかかりました。

*2:なお、この日に持って来た書籍はこの本の他にテスト駆動開発入門とレガシーコード改善ガイド

*3:当然、行き先は違う。

*4:Lesson4 敬意を払え...らしいですよ?

*5:import static org.hamcrest.core.Is.*などを適切にインポートする必要がある

*6:1toNの数字を繰り返し表示するが、数字が3の倍数の時には数字ではなくFizzという文字を、5の倍数の時はBuzzを、3と5の倍数であるときにはFizzBuzzと表示するプログラムを作成する問題

*7:別のデータを与える事でRedに戻す

*8:個人的には、このあたりも作成するクラスのコンテキストに依存するかなと思う。また、TDDのテストとは別にテストを作ってもいいわけだから、厳密なテストと軽快なテストに分けてもよいと思う。ただし、厳密なテストが軽快であるなら、それをTDDのテスト、すなわち繰り返し実行されるテストとして後から組み込んでもよいのではないだろうか

*9:C#ではDictionary, Perl/RubyではHashと言う...はず

*10:この課題はその場の思いつきで追加されたそうですw

*11:ここまで実装できたペアはあの会場にいたのだろうか……

*12:TwitterなどのIDを持っていらっしゃらなかったので、名前は一応伏せさせて頂きます。

*13:参考: http://d.hatena.ne.jp/YokoKen/20081027/1225071710

*14:他にはScala, Haskellなど盛りだくさん

*15:大体ペアプロ1回分。つまり、自分たちの2倍の速度で作っていた。