どうもABAB↑↓BAです。今回の記事はElm成分ほぼ無いです。今回の記事では謎の集団Zeneloで僕が毎週受けているTDD(テスト駆動開発)トレーニングについて、基礎中の基礎である、TDDサイクルとベイビーステップについてのみお話したいと思います。
TDDサイクルとベイビーステップ
まずテスト駆動開発とは、という前提の話をしていきましょう。テスト駆動開発では以下のサイクルを回しながら、開発を進めていくことになります。
Red -> Green -> Refactor
Red
最初に落ちるテストを書くことになります。つまりこれはどういうことでしょうか?
実装コードについてはインターフェース(実装が空だが明確に型が決まっており、テストから呼び出せる状態。)を実装します。コンパイルエラーではいけません。
class FizzBuzz {
public static String fizzbuzz(int num) {
return null;
}
}
続いてテストコードを書きます。このときは要件を設計し定義することと等しいです。テストコードはこのとき決して実装に合わせて、テストが通るコードを書いてはいけません。
public class FizzBuzzTest {
@Test
public void 数字の3はfizzである() {
assertThat(FizzBuzz.fizzbuzz(3), is("fizz"));
}
}
テストはもちろん落ちます。ここで重要なことはRedは次に実装するべきものの道を指し示し、生きている設計となります。
FizzBuzzTest > 数字の3はfizzである FAILED
java.lang.AssertionError:
Expected: is "fizz"
but: was null
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
at org.junit.Assert.assertThat(Assert.java:956)
at org.junit.Assert.assertThat(Assert.java:923)
at FizzBuzzTest.数字の3はfizzである(FizzBuzzTest.java:9)
Green
Greenは、テストを通すような最低限の実装をします。このときテストコード(設計)に間違いが無ければ一切いじってはいけません。実装を指し示された道への最短距離を突っ走ります。例えば、いくら答えがわかっていても次のようなコードを書いてはいけません。なぜ答えを書いてはいけないかは追って説明をしていきます。
public class FizzBuzz {
public static String fizzbuzz(int num) {
if(num % 3 == 0) {
return "fizz";
}
else {
return null;
}
}
}
最低限の実装とは、以下のような実装です。
public class FizzBuzz {
public static String fizzbuzz(int num) {
return "fizz";
}
}
Refactor
特に何もなければスキップします。
再びRed
次のTDDサイクルになりました。やることは同じです。落ちるテストを書いて、次の道を指し示します。
public class FizzBuzzTest {
@Test
public void 数字の3はfizzである() {
assertThat(FizzBuzz.fizzbuzz(3), is("fizz"));
}
@Test
public void 数字の5はbuzzである() {
assertThat(FizzBuzz.fizzbuzz(5), is("buzz"));
}
}
今はどんな数字が来ても"fizz"を返すマンなので間違いになります。
FizzBuzzTest > 数字の5はbuzzである FAILED
java.lang.AssertionError:
Expected: is "buzz"
but: was "fizz"
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
at org.junit.Assert.assertThat(Assert.java:956)
at org.junit.Assert.assertThat(Assert.java:923)
at FizzBuzzTest.数字の5はbuzzである(FizzBuzzTest.java:14)
2度目Green
もちろん先程のように、いきなり答えを書いてはいけません。最低限の実装だけを心がけます。頭を使ってはいけません。テンポよく次のサイクルに行くことを考えましょう。
public class FizzBuzz {
public static String fizzbuzz(int num) {
if(num == 3) {
return "fizz";
} else {
return "buzz";
}
}
}
再び無視されるリファクタリング
まあまだ良さそうですね・・・。
そろそろ分かってきたRed3
3以外の3の倍数をぶっ込んでみましょう。
public class FizzBuzzTest {
@Test
public void 数字の3はfizzである() {
assertThat(FizzBuzz.fizzbuzz(3), is("fizz"));
}
@Test
public void 数字の6はfizzである() {
assertThat(FizzBuzz.fizzbuzz(6), is("fizz"));
}
@Test
public void 数字の5はbuzzである() {
assertThat(FizzBuzz.fizzbuzz(5), is("buzz"));
}
}
else節に入って"buzz"
を返すので、Redになります。
FizzBuzzTest > 数字の6はfizzである FAILED
java.lang.AssertionError:
Expected: is "fizz"
but: was "buzz"
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
at org.junit.Assert.assertThat(Assert.java:956)
at org.junit.Assert.assertThat(Assert.java:923)
at FizzBuzzTest.数字の6はfizzである(FizzBuzzTest.java:14)
やっとこさ実装Green
最低限の実装を推し進めようと、すると以下のような意地悪なコードを延々と書くこともできますが、ここは要件に立ち返って実装をしましょう。
public class FizzBuzz {
public static String fizzbuzz(int num) {
// 流石にこれはいじわる!
if(num == 3 || num == 6){
return "fizz";
} else {
return "buzz";
}
}
}
はい。これが素直な実装ですね! なぜここでいきなり、この実装をしてはいけないかをネタバラシしましょう。numが3のケースでこの実装をしたとしましょう。そうしたらnumが6の場合のテストを書くでしょうか? 人間は基本的に楽な方へ楽な方へ心理が働くので、おそらく書かない場合が想定されるでしょう。今回はFizzBuzzがお題なので、「間違えないだろうこんなもの。」という認識でいますが人間は間違いを犯す生き物で、普段の簡単な実装も検証をサボってミスを犯しがちです。
public class FizzBuzz {
public static String fizzbuzz(int num) {
if(num % 3 == 0) {
return "fizz";
} else {
return "buzz";
}
}
}
一旦ここでテストのサイクルを止めて、まとめたいと思います。ここでの重要なポイントは、テストのサイクルをテストファースト(Redファースト)から始めることにより、実装の先行を防ぎテストによって実装がカバーされている保証を得るということ、テストが不足してしまうこと。そしてベイビーステップをすることで、テストファーストと同じ問題を解決することと、敢えて一度に対峙する問題の粒度を小さくして実装のテンポを上げること(脳みそを使わないこと)が非常に重要でした。今までTDDをしたことがない人にとっては、マインドセットの切り替えが非常に大事で、そのためトレーニングを日々積むことが大切です。練習をせずにいきなり活躍するスポーツ選手などはいません。簡単な例からTDDのサイクルを守ることと、ベイビーステップの感覚を掴むところからやってみてください。
テストの早さと量
TDDは遅いかどうか、ということがよく議題として挙げられます。テストを書く分、開発スピードは遅くなってしまうのでしょうか? それでは、先程のFizzBuzzの例で考えてみましょう。FizzBuzzを一気に実装します。そのまま納品としますか?おそらくしないでしょう。UIを経由するかCLIなのか要件はわかりませんが、おそらく実行して確認をするでしょう。
printf
デバッグしますか? もしするのであれば、おそらく以下のような手順で確認するのではないでしょうか?
- 3がfizzであること
- 次に5がbuzzであること
- 立ち返って 6がfizzであること
あれあれ? 何か見覚えがありますね・・・。そうです。TDDとは、実際に自分がアプリケーションを確認するフローと同じなのです。違いは何でしょうか? 実行確認はその場限りの検証でしかありません。コードに変更が起きたときには、その確認は無意味になりますし確認したのは「あなた」だけになります。これが実プロダクトのような機能が複雑に絡み合ったものを想像してみましょう。TDDしますか? 手動確認しますか?
テストの量については基本的に、自分の不安が無くなるまで書き続けます。その上でテストコードで重複コードが大きくなってくるでしょう。そのときは適切なリファクタリングやParameterizedテストなどを試すと良いでしょう。