TDD Bootcamp の体験談を聞いていたら、是非参加してみたいとおもっていました。
そうすると、 TDD Boot Camp 北陸 なるものが開催されるということで、参加してきました。
北陸エンジニアグループ,katzchangさん、t_wadaさん、その他参加者の皆さんありがとうございました。
TDD BootCampに参加して
TDDと自分
じつは、TestUnitがある!ということ、TDDという言葉がある事は知っていましたが、 実際にTDDを実体験するのは初めてでした。
そんな自分が、今回TDD Boot Camp 北陸に参加して、疑問に思う事を沢山質問してきました。
その一部を記録として残しておきたいと思います。
TDDで生じた疑問とその議論結果
講演、実際のペアプロを2回で疑問に思った事とその議論結果についてまとめておきます。
t_wadaさんを含め、議論に参加頂いた方に感謝します。
黄金の回転について
教えていだたいた事
TDDについて以下のような手順を頂きました。
- テストを書き Red を確認
- テストを通す、明確な実装を書きGreenを確認
- リファクタリング
- テストを通し、Greenを確認
最初に戻る。
この点は凄く理解できたのですが、その実現過程で疑問が生じました。
例
まず最初にテストを書く
void test_三の倍数だとFuzzが返却される(){
assert( "Fuzz" , fuzzbuzz( 3 ) );
}
最初に assert( "Fuzz" を先に書く、 fuzzbuzzのI/F等は後から考えるという主張を面白く考えました。
このテストを通し「Red」を確認して
そのあとに、明白な実装を書く
string fuzzbuzz(int n){
return "Fuzz";
}
このテストを通し「Green」を確認。
この手順で書いていくという流れを教えていただきました。 (これで、テストのテストも行えると)
これで、1黄金回転が終わったので、次の回転ということで
void test_三の倍数だとFuzzが返却される(){
assert( "Fuzz" , fuzzbuzz( 3 ) );
}
void test_1だとFuzzが1される(){
assert( "1" , fuzzbuzz( 1 ) );
}
のテストを書き、Redを確認、その後自明な実装をしてしまう
string fuzzbuzz(int n){
if ( n % 3 == 0 )return "Fuzz";
//API名失念ですが、整数を文字列に変換する標準のクラスライブラリ
return String.ToString(n);
}
このような実装を行い、"green"確認。
発生した疑問!!
開発者の視点からすると、
void test_三の倍数だとFuzzが返却される(){
assert( "Fuzz" , fuzzbuzz( 3 ) );
assert( "Fuzz" , fuzzbuzz( 6 ) );
assert( "Fuzz" , fuzzbuzz( 9 ) );
}
のようなテストコードを書きたくなるなと思う。
しかし、既に
void test_三の倍数だとFuzzが返却される(){
assert( "Fuzz" , fuzzbuzz( 3 ) );
}
void test_1だとFuzzが1される(){
assert( "1" , fuzzbuzz( 1 ) );
}
と
string fuzzbuzz(int n){
if ( n % 3 == 0 )return "Fuzz";
//API名失念ですが、整数を文字列に変換する標準のクラスライブラリ
return String.ToString(n);
}
で、Greenになっている。 この状況で、他の3の倍数を追加しても、「Green」のままで、テストのテストである Redが書けない事になるのではないか?
ということが疑問点です。
テストの順番
この場合、例が単純すぎるかもしれないけれども、 先に3の倍数の時に、テストが失敗する例をもう少し上げておき、 その後にその他のテストをした方が 良いのではないかという事を議論させていただきました。
動的言語とredになるテスト
私は、持参した環境にrubyしか入っていなかったので、ruby言語でTDD体験してきました。 (今思えば、ペアの方のPCを借りるとC#でも体験できたのではないかと思い、少し残念でした)
今回のお題
あるメッセージを検閲して、フィルタする WordFilterを作るというのがお題でした。
WordFilter に NG Word を登録しておき、メッセージをそのWordFilterにその語を検知(detect)して含まれている事を確認する。 またフィルターして <censored> に置換する物を作りましょう。というものでした。
発生した疑問
rubyで体験するということで、どうしても出てくるのが、動的言語という点です。
黄金の回転を回していくという事で以下のような手順を通すかと思います。
例えば、検知(detect)メソッドをTDDで実現していく際に
def test_detect
assert( wordfilter.detect("NGWord を含むメッセージ") )
end
というテストを書きます。このテストを赤くする方法として、
class WordFilter
def detect( message )
nil
end
end
という方法と
class WordFilter end
という方法があります。
前者は、nil が返却されるために red になります。 後者は、メソッドがないために、実行時エラーが発生して red になります。
コンパイラが介在する言語だと、コンパイルエラーに相当する部分がありますが、 それを動的言語では実行時エラーとして検出されることになります。
この実行時エラーをもって、テストが Red になったというのは、妥当なのか? という疑問が出てきました。
議論結果
- 動的言語では、実行時エラーも重要なので、この失敗をもって red とし、greenになる実装に移る
- 実行時エラーを検出して(red)、その後 nil を返す実装をして(red) その後、greenになる実装に移る
この2つがあるが、後者ではく前者で十分ではないかという議論になりました。
議論をしていて思ったのが、後者のred -> red -> greenはより厳密だとおもうのですが、スピード感が失われるからかなと。
内部の実装が透けて見えてしまう!?
オブジェクト指向だと隠蔽化で処理が隠れる利点(場合によっては欠点?)があります。
また、t_wadaさんの講演で「テストは内部実装に依存するようなものを書かない」という 事もありました。
それを踏まえて、以下の疑問点と議論がでてきました。
仕様変更
先ほどの WordFilterに複数単語を登録して、検出、検閲出来るようにしてほしい。 また、後から単語登録が出来るようにしてほしい。
このような依頼がきました。
内部実装が透けて見えるテスト
複数単語が登録できるということで
def test_add_words
wordfilter = WordFilter.new("単語1","単語2")
wordfilter.add("単語3","単語4")
assert_equal( ["単語1","単語2","単語3","単語4"] , wordfilter.words )
end
このようなテストを書いてみました
疑問点1
この
assert_equal( ["単語1","単語2","単語3","単語4"] , wordfilter.words )
は、内部実装が 配列で実現している事を明確にしているのではないかということです。 確かに、WordFilter は 内部で配列を持ち単語群を管理する実装になっていますが、 たまたまなわけです。将来ハッシュで管理したくなるかもしれません。 (実際、単語の頻度を保持してほしいとか仕様変更がありましたし、それに伴い Hash化されるかもしれません)
この内部実装変更で、リストで管理しているという事、及び順番まで保証するようなテストは いかがなものか?
議論結果
確かに、それは問題なので、可能ならこれを変えた方がいいという事で、以下のように変えました。
def test_add_words
wordfilter = WordFilter.new("単語1","単語2")
wordfilter.add("単語3","単語4")
assert( wordfilter.words.include?("単語1") )
assert( wordfilter.words.include?("単語2") )
assert( wordfilter.words.include?("単語3") )
assert( wordfilter.words.include?("単語4") )
end
内部実装を知ったテスト
複数単語が含まれているか?をチェックする為に、実装として 単語集合から正規表現を構築してそれでマッチを行っていました。
追加したテストと疑問点
ただ、正規表現の文字列のエスケープ処理が必要になるのではなかということが 開発者視点で気になり以下のコードを追加しました。
def test_detect_with_specialchar
wordfilter = WordFilter.new("/")
assert( wordfilter.detect("/") )
end
これは、内部が正規表現を使っている事を前提にテストをしているから、 いかがなものなのか?
これが疑問になりました。
議論の結果
これは、内部で正規表現を使っていても、使っていなくても通るベキコードなので 内部に依存したテストではないという事になりました。
事後の議論
帰りの車の中で、@fujiwoさんとお話していて、このパターンをテストケースに入れれるかどうか というのは、悩ましいという話になりました。
デベロッパ視点だから、いれれるのでしょうけど、経験を持った人でないと入れれないよねという 話も出ました。
TDD BootCamp 北陸に参加して
- 久々にコードを沢山書いて、設計議論が出来て面白かった
- TDDは疲れる(だから悪いとか、良いとかいわない)
- ペアプロは面白い
凄く楽しかったです。
また、t_wada賞を頂きました。 個人的には「割と普通とは違う視点で議論してた」というポイントから頂けたのではないかと 思いました。
皆さん、ありがとうございました。
(後日談に続きます)


コメントする