はじめに
※本記事は t-wadaさん もしくは TDD Boot Campさん に怒られたら消します。(雀の涙程度のオリジナリティ1はあるものの、コンテンツ自体はt-wadaさんの発表に全乗っかりしてるので・・・)
正直、こんな記事を読むよりも、t-wadaさんが翻訳した書籍買って、t-wadaさんが発表してる動画を見た方がよっぽど良いと思います。
特に書籍では、TDDを超えてBDDなどの話も綺麗にまとまっている付録が読めるので、強くお勧めします。みんなも買おう!
本記事では書籍の内容には触れませんが、この記事よりもかなり実践的な事例が載っているので、実務開発を考えると読むことをお勧めします。
主催のTDD Boot Campさんは、ここ一年くらいは活動されてなさそうですが、一応コミュニティのリンク貼っておきますね。
記事について
本記事は、t-wadaさんの「TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング」で行われていたライブコーディングを、ブラウザだけで実践してみる記事です。
大きくは以下の流れで進んでいきます。
- TDDについて概説(ざっくり座学)
- Cyber-dojo(ブラウザでTDDが実践できるWEBサイト)で「FizzBuzz」をTDDしてみる。(記事の9割がここ)
- おさらい
細かい流れは目次見てくれればいいっす。え、スマホは目次が無いって?
TDDの実践を目的とした記事なので、ぜひPCで見てほしい。
スマホ派は、とりあえず「総まとめ」だけ読んでもらって、良きタイミングでPCで見直していただけると嬉しいです。
注意事項
「Javaは知ってるけど、TDDはよく知らない」
って人が「TDDを習得すること」
を目的に、記事を作成しております。
- TDDは知らなくても大丈夫です。
- Javaは、特にコアな話は出てこないので、ちょっとでもコードが読めれば大丈夫だと思います。
- 初心者向けタグはつけましたが、流石にプログラミング未経験者がわかるレベルにはなってないです。読み始めて分からないと感じたら、またのご来訪をお待ちしております。
「TDDってやったことあるけど、やりづらくて、俺には合わなかったんだよねー」
って人もぜひ見てってほしい。
- スキルがある方は、特に「Green」→「Refactor」とステップを分けることにストレスを感じると思います。
- TDDって実は、開発者のスキルに応じたテクニックが用意されていて、スキルフルな方向けに「明白な実装」というテクニックがあります。
- 今回は「4つ目のサイクル」で実践してますが、いきなり読んでも文脈わからんと思うので、ぜひ手を動かしながら身につけていただければと思います。
スクロールバーを見てください。
長いよ。(動画見るだけでも2時間かかるやつだからね)
- とにかく結論だけ簡潔に教えろって人は「総まとめ」を見てくれれば良いです。
- ただ、手を動かしてもらうことを目的に記事作ったので、できれば頭から読んで手を動かしてもらえると嬉しい。
- スマホな方は、とりあえず「総まとめ」だけ読んでもらって、良きタイミングでPCで見直していただけると嬉しいです。
お願い事項
「テスト駆動開発」は兎にも角にも「手に馴染ませる」ことが重要だと思います。
慣れないうちは少しやりづらさも感じるテクニックですが、TDDのやり方を習得すると、今まで以上に効率よく開発できるようになると思います。
だからこそ、誰でも簡単に「手を動かす」ことを試せる「ブラウザだけでできる」ということにフォーカスして記事を作成しました。
ということで、「ぜひこの記事を見ながら手を動かしてもらいたい!」というのがお願い事項になります。
TDD概説:t-wadaさんスライドをベースに
t-wadaさんの動画をすでにみたなら飛ばしていいところです。
スライドもそのまま使わせてもらうスタイル。
補足説明は自分の言葉で説明しちゃってます。
(t-wadaさんの説明が聞きたい人は、冒頭のリンクからyoutubeアーカイブに行っていただければ・・・)
- 2つの道
- 上の道:最初に「綺麗にする」、次に「動作させる」
- 下の道:最初に「動作させる」、次に「綺麗にする」
- TDDでは「下の道」を選ぶアプローチ。
- 「動作する」を乗り越えるのが本当に大変。
- どんなに事前に綺麗に設計したつもりでも、動作しないことはありうる。
- だから下の道の方が筋が良さそう。
- というのがTDDの考え方。
- まずは何を実装しようとしているのか、todoリストに落とし込む。
- todoリストができたら、そのうち1つを選んで、上記スライドの通り対応していく。
- ここら辺の手順は実践の中でもう少し丁寧に説明します。
TDDを実践する前準備
TODOリストを作成する
今回のお題はFizzBuzz問題を使います。
プログラミングでよく使われる簡単な練習問題です。
1から100までの数をプリントするプログラムを書け。
ただし3の倍数の時は数の代わりに「Fizz」と、5の倍数の時は「Buzz」とプリントすること。
3と5両方の倍数の場合には「FizzBuzz」とプリントすること。
これを紐解いてTODOにするのにはスキルが必要ですが、この記事ではそこはフォーカスしません。(「ブラウザだけで実践したらどうなるのか?」にフォーカスしたいので)
TODOの作り方が知りたい人は、t-wadaさんの動画みてください。
※かなり重要なポイントなので、ぜひ見ましょう。(以下リンクはちょうどその説明が始まるあたりです。)
今回のTODOリストを作ってる様子(0:44:10〜0:56:02)↓
TODOリストのスキルを上げる方法について語っている部分(1:38:51〜1:39:21)↓
以下、結果です。(ました工法)
テスト容易性:高 重要度:高
- [ ] 数を文字列に変換する
- [ ] 3の倍数の時は数の代わりに「Fizz」に変換する
- [ ] 5の倍数の時は数の代わりに「Buzz」に変換する
- [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する
テスト容易性:低 重要度:低
- [ ] 1からnまで
- [ ] 1から100まで
- [ ] プリントする
Cyber-dojo準備
実際に手を動かしながらできるように、ちょっとしたサービスを使います。
Cyber-dojoという、自分のPC上に環境を作らなくても、WEBブラウザだけでTDDを実践できるWEBサービスを使います。
ローカルでIDEを使って開発するのに比べたら不便もありますが、ブラウザだけで開発できるのは非常に魅力的です。
環境準備はすごく簡単です。3〜4クリックで作れちゃいます。ありがたい限りです。
「create a new practice」のボタンを押下し、以下条件で作成します。
- exercise: Fizz Buzz
- language & test-framework: Java 17, jUnit
- practice type: solo
確認画面が出るので、「OK」を押したら以下のような画面が表示されるはずです。
この画面をブックマークしておきましょう。間違ってブラウザを閉じても途中状態が保存されているので、ブックマークから戻れば元通りです。
(通信状態や操作内容によっては、多少の手戻りはあるかもしれませんので、悪しからず。)
ちなみに、Cyber-dojoは商用利用の場合はライセンス料を支払う必要があります。
非商用利用(個人での学習目的など)であれば不要ですが、寄付は募っているようです。
素晴らしい環境ですので、余裕のある方は寄付をお願いします。
最初のTDDサイクルを回す
テストクラス・テスト対象クラスを作ったりとか色々やらないといけないステップです。
IDEの機能を使うと簡単に実装できる部分が多いのですが、Cyber-dojoではサポートされてないものが多いです。
この記事ではCyber-dojo環境に適した形での「最初のTDDサイクル」を考えてみます。
なお、TDDサイクルは以下の手順で進みます。
TODOを選ぶ
テスト容易性:高 重要度:高
- [ ] 数を文字列に変換する
- [ ] 3の倍数の時は数の代わりに「Fizz」に変換する
- [ ] 5の倍数の時は数の代わりに「Buzz」に変換する
- [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する
テスト容易性:低 重要度:低
- [ ] 1からnまで
- [ ] 1から100まで
- [ ] プリントする
今回は一番上のやつを選びましょう
- [ ] 数を文字列に変換する
Red:初めてのレッド
一番最初にやるべきこと
最初は何もありません。テスト対象もテストコードもありません。
ということで、テストクラスを作るところから始めていきましょう。
テストクラスを作成する
IDEなら半自動で作ってくれるのですが、Cyber-dojoでは手動で作る必要があります。
IDEでの動きはt-wadaさんの動画でご確認ください。(0:57:21〜1:01:10)
今回はCyber-dojoでの実施ですので、すでに存在するHikerTest
くんを修正する形で、全手動で作りましょう。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class HikerTest {
@Test
void life_the_universe_and_everything() {
int expected = 42;
int actual = Hiker.answer();
assertEquals(expected, actual);
}
}
今回作成するのはFizzBuzz機能。それをテストするクラスなので、クラス名としてはFizzBuzzTest
あたりが妥当でしょうか。
書き直してみます。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
-public class HikerTest {
+public class FizzBuzzTest {
@Test
void life_the_universe_and_everything() {
int expected = 42;
int actual = Hiker.answer();
assertEquals(expected, actual);
}
}
Javaではファイル名と不一致だとエラーになってしまうので、ファイル名も直しましょう。
Cyber-dojoではファイルを選択した上で、以下のボタンを押下すると名前を変えられます。
無事リネームできたら以下のようになります。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void life_the_universe_and_everything() {
int expected = 42;
int actual = Hiker.answer();
assertEquals(expected, actual);
}
}
コンパイルエラーが無いか確認してみましょう。
テスト実行は左上にある「test」ボタンです。
結果はこんな感じになるはずです。
:stdout:
.
+-- JUnit Jupiter [OK]
| '-- HikerTest [OK]
| '-- life the universe and everything [X] expected: <42> but was: <54>
'-- JUnit Vintage [OK]
Failures (1):
JUnit Jupiter:HikerTest:life the universe and everything
MethodSource [className = 'HikerTest', methodName = 'life_the_universe_and_everything', methodParameterTypes = '']
=> org.opentest4j.AssertionFailedError: expected: <42> but was: <54>
org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62)
org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150)
org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:145)
org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:527)
HikerTest.life_the_universe_and_everything(HikerTest.java:15)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:568)
[...]
Test run finished after 134 ms
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 3 containers successful ]
[ 0 containers failed ]
[ 1 tests found ]
[ 0 tests skipped ]
[ 1 tests started ]
[ 0 tests aborted ]
[ 0 tests successful ]
[ 1 tests failed ]
:stderr:
:status:
1
ひとまず、outputの上の方に以下が表示されていれば問題ないです。以下が表示されていない場合は、どこか間違えています。
エラーメッセージを確認して修正しましょう。
+-- JUnit Jupiter [OK]
| '-- HikerTest [OK]
| '-- life the universe and everything [X] expected: <42> but was: <54>
'-- JUnit Vintage [OK]
jUnitの稼働確認
開発環境が出来上がって一番最初にやることは、テストツールの稼働確認です。
テストツールが正しく動いていることが保証されなければ、テスト結果が正しいと保証できません。
このスタートラインに立てていることを確認していないと、余計なことを調査することになり、不必要に時間がかかります。
非常に重要なステップです。
さっきjUnit動かしたじゃないか!と言う話はあるんですが・・・
デフォルトで用意されているテストメソッドは自分が書いたコードではないので、出力結果が期待通りなのか自信が持てません。
なので、自分でテストメソッドを書きます。
jUnit稼働確認のテストは「必ず失敗する」テストを書きます。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
- void life_the_universe_and_everything() {
- int expected = 42;
- int actual = Hiker.answer();
- assertEquals(expected, actual);
- }
+ void test() {
+ fail("失敗するはず");
+ }
}
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void test() {
fail("失敗するはず");
}
}
このコードは以下を期待して書かれたコードになります。
- テストが動く
- テスト結果は失敗
- "失敗するはず"と表示される
実際に動かしてみましょう。
結果はこんな感じになるはずです。
:stdout:
.
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| '-- test [X] 失敗するはず
'-- JUnit Vintage [OK]
Failures (1):
JUnit Jupiter:FizzBuzzTest:test
MethodSource [className = 'FizzBuzzTest', methodName = 'test', methodParameterTypes = '']
=> org.opentest4j.AssertionFailedError: 失敗するはず
org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39)
org.junit.jupiter.api.Assertions.fail(Assertions.java:134)
FizzBuzzTest.test(FizzBuzzTest.java:13)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:568)
org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
[...]
Test run finished after 118 ms
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 3 containers successful ]
[ 0 containers failed ]
[ 1 tests found ]
[ 0 tests skipped ]
[ 1 tests started ]
[ 0 tests aborted ]
[ 0 tests successful ]
[ 1 tests failed ]
:stderr:
:status:
1
期待と比較しましょう。
- テストが動く:○
- テスト結果は失敗:○
- "失敗するはず"と表示される:○
大成功です!!!
ポイント
- 開発環境が作れたら一番最初にやるのは「テストツール」の稼働確認
- 稼働確認が取れていない場合で、テストツール起因の失敗を引いてしまった場合、開発着手に大幅な遅延が生じてしまう。
- そういった大幅な遅延を避けるための重要なステップ。
- 稼働確認では「期待通り失敗すること」を確認する
- 期待通りというのは
fail("メッセージ")
を使うことで簡単に確認できる
- 期待通りというのは
個人的な疑問に対する、個人的な理解
Q: なぜ失敗させるの?成功でも良いんじゃない?そっちのが気分いいじゃん。
A: 一度にいろんなことが確認できるので、fail()を使っている。
- 成功を確認しようとした場合
- そもそもfail()の反対になるようなメソッドがない。(success()みたいなのは無い)
- 成功させるにはassertを書く必要があるが、assertを書くほどコストをかける意味が無い。
- fail()の良いところ
- メッセージ出力を確認できるため、自分の実装したコードが動いたことを間違いなく確認できる。
- 気分の問題
- これはもう「失敗に慣れるしかない」と思う。
- 「失敗」って表現は悪いように聞こえるかもしれないけど、「失敗することが分かった」という捉え方に変えていくのが良さそう。
- そういうスタンスに立って着実に前に進んでいくのがTDDだと思う。
TODOに着手する
さてTODOを選んだ直後に、TODOから離れたことをやってしまいました。
ここから本格的にTODOに着手しますので、思い出しましょう。
- [ ] 数を文字列に変換する
テストメソッドの名前を決定する
今回はtodoをそのままメソッド名としましょうか。
- [ ] 数を文字列に変換する
t-wadaさんはテストメソッド名は日本語で書くことを推奨しています。
日本人メンバーだけで開発する状況であれば、こちらの方が可読性が高いからです。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
- void test() {
- fail("失敗するはず");
- }
+ void 数を文字列に変換する() {
+ }
}
テストは下から書く
テストの中身を実装していきましょう。
テストは基本的には以下のような構造で実行されます。
各タイミングで何をするかをテストコードに記載する必要があります。
- 準備
- 実行
- 検証
TDDでは下から書いていきます。「検証」から書いていきます。
こうすることで、無駄なく実装することができます。
検証の実装:期待値を書く
検証はassertEquals(expected, actual);
を使うので、以下のように書きます。
※残念ながらCyber-dojoではサジェスト機能がありません。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void 数を文字列に変換する() {
+ //準備
+ //実行
+ //検証
+ assertEquals(expected, actual);
}
}
ここで問題が生じます。
「数を文字列に変換する」ために何と何を比較すれば、できたことになるのでしょうか?
実はこの「数を文字列に変換する」では具体性がまだ足りなかったことに気づきます。
机上の設計だけでは、ついこういったことが起こりがちです。
それを実装しながら、具体的に確かめながら進んでいけるのが、TDDの良いところです。
もっと具体的な内容でTODOを考え直しましょう。
- [ ] 数を文字列に変換する
- [ ] 1を渡すと文字列"1"を返す
このtodoなら、期待値を実装するのは迷わないでしょう。
assertEquals(expected, actual);
をassertEquals("1", actual);
に書き換えましょう。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
//実行
//検証
- assertEquals(expected, actual);
+ assertEquals("1", actual);
}
}
実行の実装→準備の実装
これから実装することを頭に思い浮かべます。
きっとこんな感じでしょう。
「テスト対象」にある「何かしらのメソッド」に「数値1」を渡すと「文字列1」が返ってくる。
これをひとまずそのまま書いてみます。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
//実行
+ String actual = テスト対象.何かしらのメソッド(1);
//検証
assertEquals("1", actual);
}
}
ちょっとそのまま過ぎますが、まずはアウトプットするのが大事です。
ただこの命名はいただけません。考え直していきましょう。
「テスト対象」は、FizzBuzz機能を実装するクラスなので、「FizzBuzz」とか良いんじゃ無いでしょうか?
「何かしらのメソッド」は、数値を文字列に変換するメソッドなので、「convert()」とかどうでしょうか?
※命名は使う人にとってわかりやすい命名にしましょう。これがベストと言っているわけじゃ無いです。
ということで、そのまま置き換えてみましょう。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
//実行
- String actual = テスト対象.何かしらのメソッド(1);
+ String actual = FizzBuzz.convert(1);
//検証
assertEquals("1", actual);
}
}
おっと、Javaはインスタンス化して使うのが基本でしたね。書き直しましょう。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
+ FizzBuzz fizzbuzz = new FizzBuzz();
//実行
- String actual = FizzBuzz.convert(1);
+ String actual = fizzbuzz.convert(1);
//検証
assertEquals("1", actual);
}
}
ひとまずテストコード側の準備はできましたが、このままでは実行できません。
存在しないクラスを対象にしているため、コンパイルエラーが発生しています。
この次はコンパイルエラーを解消していきます。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行
String actual = fizzbuzz.convert(1);
//検証
assertEquals("1", actual);
}
}
下から書くことの効能
記事作成者の個人的な体験をベースにした追記です。
この「テストを下から書く」と言うテクニックはペアプロ・モブプロの時に本当に強力な効果を発揮します。
ペアはまだマシですが、モブやってる最中、スキルが低い立場になると、まじで何やってるかわかんない時が出ちゃいます。
正確には「なんとなくわかった気になっちゃってる」とでも言えばいいでしょうか。
でも、この下から書くやり方だと、「何を目的に今実装を行なっているのか」が非常に明確になるので、そういう半分離脱してしまうような状況を防ぐことができます。
ペアやモブをやるからには、参加者にはスキルを身につけてほしいし、参加者のノウハウをコードに全部入れたい。
全員が前のめりで参加できる仕組みを整えてくれるTDDの効能は本当に大きなものがあります。
コンパイルエラーを解消させる
さて、実際のコードに戻りましょう。現在はコンパイルエラーが発生していましたね。
一般的なIDEであれば自動作成の機能があるので、エラーのサジェストから簡単に作れます。
これもテストコードから書く利点です。あれこれ手動で作成する必要がありません。
IDEができることはIDEに任せればいいのです。
ただ、Cyber-dojoにそんな便利な機能はありません。
IDEでの動きは、t-wadaさんの動画で見てみましょう(1:14:11〜1:17:41)↓
戻ってきまして、ここはCyber-dojoです。Cyber-dojoらしく、手動でやっていきましょう。
HikerTest.java
をFizzBuzzTest.java
に変更したように、Hiker.java
をFizzBuzz.java
に変えていきましょう。
実際はIDEが自動でやってくれる部分なので、解説は省略して、ました工法でいきます。
public class Hiker {
public static int answer() {
return 6 * 9;
}
}
public class FizzBuzz {
public String convert(int i) {
return null;
}
}
Cyber-dojoではコンパイルエラーが無くなったかどうかは、動かしてみないとわかりません。
なので、ひとまず動かしてみましょう。
(実際の場面でも、コンパイルエラーが無くなった段階で、一度テスト実行します。)
ボタンの説明はもういいですね。今後割愛します。
:stdout:
.
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| '-- 数を文字列に変換する [X] expected: <1> but was: <null>
'-- JUnit Vintage [OK]
Failures (1):
JUnit Jupiter:FizzBuzzTest:数を文字列に変換する
MethodSource [className = 'FizzBuzzTest', methodName = '数を文字列に変換する', methodParameterTypes = '']
=> org.opentest4j.AssertionFailedError: expected: <1> but was: <null>
org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62)
org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1141)
FizzBuzzTest.数を文字列に変換する(FizzBuzzTest.java:18)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:568)
[...]
Test run finished after 157 ms
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 3 containers successful ]
[ 0 containers failed ]
[ 1 tests found ]
[ 0 tests skipped ]
[ 1 tests started ]
[ 0 tests aborted ]
[ 0 tests successful ]
[ 1 tests failed ]
:stderr:
:status:
1
ひとまず序盤に記載されている以下が確認できればいいでしょう。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| '-- 数を文字列に変換する [X] expected: <1> but was: <null>
'-- JUnit Vintage [OK]
「1を期待したのに、nullが返ってきたよ!」
ということで、予定通りコンパイルエラーは無くなり、想定した通りのエラーが返ってきています。
順調に進んでいます!
Green:最初のグリーンと「仮実装」
ここまでで1〜3が終わった状態です。今から4〜5をやっていきます。
このグリーンは「最短」で実現させます。どういう意味かこれから説明します。
現時点のテストコードは以下な感じです。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行
String actual = fizzbuzz.convert(1);
//検証
assertEquals("1", actual);
}
}
actual
に"1"
が入ればグリーンになりますね。
ということで、テスト対象のコードを以下に修正します。
public class FizzBuzz {
public String convert(int i) {
- return null;
+ return "1";
}
}
public class FizzBuzz {
public String convert(int i) {
return "1";
}
}
さぁテストを実行しましょう。
:stdout:
.
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| '-- 1を渡すと文字列1を返す [OK]
'-- JUnit Vintage [OK]
Test run finished after 154 ms
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 3 containers successful ]
[ 0 containers failed ]
[ 1 tests found ]
[ 0 tests skipped ]
[ 1 tests started ]
[ 0 tests aborted ]
[ 1 tests successful ]
[ 0 tests failed ]
:stderr:
:status:
0
おめでとうございます!成功です!
ちなみに、成功かどうかは以下全てが[OK]
となっていることで確認できます。
(outputも冗長に感じるので、今後は以下のような省略版で記載するようにします。)
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| '-- 1を渡すと文字列1を返す [OK]
'-- JUnit Vintage [OK]
return "1";
なんて、ひどい茶番に見えると思いますので、少し解説を入れます。
「仮実装」
もし、今回のテストが失敗していたとしたら、バグが潜んでいるのはどちらでしょうか?
テスト対象?それともテストコード?
public class FizzBuzz {
public String convert(int i) {
return "1";
}
}
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行
String actual = fizzbuzz.convert(1);
//検証
assertEquals("1", actual);
}
}
「テスト対象」はreturn "1";
しか書かれていません。ここにバグが混入するとしたら、Javaの内部処理を疑うことになるでしょう。
ここまでシンプルなコードだと、Javaの内部処理が間違ってるとも思い難いです。
となると、「テストコード」にバグが混入しているのでしょう。
我々はテスト対象の正しさを確かめるためにテストコードを書きます。でもテストコードが正しいことは、誰が保証してくれるのでしょうか?
「テストコードのテストコード」を書きますか?じゃぁその「テストコードのテストコードの正しさは・・・」(以降エンドレス)
この一見馬鹿馬鹿しいやり方は「テストコードの正しさ」を検証するやり方なのです。
このテクニックをTDDでは「仮実装」と呼びます。
この「仮実装」は毎回行うテクニックではありません。
後で紹介しますが、「三角測量」や「明白な実装」といった別のテクニックもありますので、それらを理解した上で、必要に応じて使い分けていきましょう。
「Defect Insertion」
おまけでさらにもう一つのテクニックです。「Defect Insertion」と呼ばれる、あえて誤りを混入させることで、期待通りに動いていることを確認するテクニックです。
今回のコードで「Defect Insertion」を行うなら、以下のようにコードを変更しましょう。
public class FizzBuzz {
public String convert(int i) {
- return "1";
+ return "0";
}
}
public class FizzBuzz {
public String convert(int i) {
return "0";
}
}
ここでテスト実行すると以下のようになります。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| '-- 1を渡すと文字列1を返す [X] expected: <1> but was: <0>
'-- JUnit Vintage [OK]
ここまでやっておけば、テストコードがどのように動くかを確実に確認できたと言えるでしょう。
確認ができたので、グリーンになるようにコードを戻しましょう。
public class FizzBuzz {
public String convert(int i) {
- return "0";
+ return "1";
}
}
public class FizzBuzz {
public String convert(int i) {
return "1";
}
}
テスト実行も忘れずにやりましょう。確認が大事です。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| '-- 1を渡すと文字列1を返す [OK]
'-- JUnit Vintage [OK]
Refactor
STEPの6に入っていきます。
public class FizzBuzz {
public String convert(int i) {
return "1";
}
}
テスト対象コードはシンプルすぎてリファクタリングするところが無いですね。
// A simple example to get you started
// JUnit assertion - the default Java assertion library
// https://junit.org/junit5/
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行
String actual = fizzbuzz.convert(1);
//検証
assertEquals("1", actual);
}
}
テストコードは色々気になります。まずは冒頭のコメントが邪魔なので消しますか。
-// A simple example to get you started
-// JUnit assertion - the default Java assertion library
-// https://junit.org/junit5/
-
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行
String actual = fizzbuzz.convert(1);
//検証
assertEquals("1", actual);
}
}
String actual = fizzbuzz.convert(1);
と一度変数に入れてますが、ちょっと冗長に感じますね。
assertEquals("1", fizzbuzz.convert(1));
って書いちゃえば良さそうです。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行
- String actual = fizzbuzz.convert(1);
//検証
- assertEquals("1", actual);
+ assertEquals("1", fizzbuzz.convert(1));
}
}
コメントに違和感が出てくるので、そこも修正しましょう。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
- //実行
- //検証
+ //実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
}
ここまで修正すると、以下な感じになります。少しスッキリしましたね。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
}
テストも忘れずに実行します。全部OKで返ってくることを確認しましょう。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| '-- 1を渡すと文字列1を返す [OK]
'-- JUnit Vintage [OK]
Cyber-dojo上でのテスト実行は遅いので、若干まとめて実行する動きになってますが、実際は一つの修正ごとにテスト実行するのが望ましいです。
そうすることで、「今まさにバグを混入したタイミング」を掴むことができます。
そうすれば、どこを修正すればいいかは明白です。
バグ修正で一番しんどいのは原因の特定です。
頻繁にテストを回しながら実装することで、その労力を大幅にカットすることができます。
今回のコードは非常にシンプルなので、その恩恵がわかりづらいですが、実業務の複雑なコードを対象にした場合、この効果は非常に大きくなります。
最初のサイクルのまとめ
最初のサイクルについて
- 最初のサイクルは、非常にやることが多い。
- テストクラスの作成
- テストツールは正しく動くのか?
- テスト対象クラスの作成
- だからこそ、テスト容易性の高いTODOを選ぶことが大事。
- 副作用としてひどいコード
return "1";
ができるが、それは以降のTODOで解消していく。
テクニックについて
- 最初のテストは必ず失敗するテストを書く:jUnitが動くことを確実に確認するために
fail("メッセージ")
を使う。 - テストは下から書く:下から書くことで、実装目的を明確にできるし、IDEの機能のおかげで効率よく実装できる。
- TDDはペアプロ・モブプロに特に効く:テストを下から書くことも合わさり、実装目的が非常に明確になるため、ペアプロ・モブプロ時の半離脱状態を防げる。
- 仮実装:
return "1";
といったようなテストを通す最小限の実装を指す。テストコードのテストという意味合いもある。 - jUnitを何度も回しながらリファクタリングする:バグ混入タイミングで検知できるため、修正が容易。TDDによる非常に大きな恩恵の一つ。
現時点のコードも改めて載せておきます。
public class FizzBuzz {
public String convert(int i) {
return "1";
}
}
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
}
テスト容易性:高 重要度:高
- [ ] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す
- [ ] 3の倍数の時は数の代わりに「Fizz」に変換する
- [ ] 5の倍数の時は数の代わりに「Buzz」に変換する
- [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する
テスト容易性:低 重要度:低
- [ ] 1からnまで
- [ ] 1から100まで
- [ ] プリントする
2つ目のサイクルを回す
TODOリストの確認
- [ ] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す
「1を渡すと文字列"1"を返す」というタスクは終わったので「x」をつけています。
でも、これで親タスクである「数を文字列に変換する」が終わったとは言えません。
「三角測量」というテクニックでこの親タスクの達成を目指します。
「三角測量」は個人的理解を言えば、データバリエーションを2つ用意し(点1・点2)、この点1・点2に基づいて、目指したい状態(点3)を実現するやり方です。
単一のテストだけでは不安がある時に用いるテクニックです。
今回は一番最初の実装ということもあり、テストが正しく実装できているのか不安ですので、このテクニックを用いてみましょう。
今回は以下のようなTODOリストとなります。
- [ ] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す
- [ ] 2を渡すと文字列"2"を返す
「2を渡すと文字列"2"を返す」を実装しつつ、「数を文字列に変換する」を実装していきます。
Red
アンチパターン:アサーションルーレット
その前に、よくあるアンチパターンを説明していきます。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
+ assertEquals("2", fizzbuzz.convert(2));
}
}
こうやって、一つのテストメソッドに対して、複数のassertを重ねるやり方は、「アサーションルーレット」と呼ばれます。(ロシアンルーレットのメタファー?)
この場合は「(デバッグしないと)どのassertで失敗したか分からない」ということを意味しています。
また、失敗した以降のassertは実行されないので、もしかしたら以降のassertが失敗してるかもしれず、目の前のバグを直しても終わらないという沼に入ってしまうことがあり得ます。
ただし、これがどんな場面でもアンチパターンかというと、そうでない場面もあり得ます。(E2Eテストなど)
ただ、まずは疑ってかかるのが良いスタンス。
そうでない場面については、動画でt-wadaさんが答えているので見てみましょう(1:39:26〜1:41:20)↓
2つ目のテストを書く
ひとまず、望ましい書き方で2つ目のテストを書いてみましょう。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
+
+ @Test
+ void _2を渡すと文字列2を返す() {
+ //準備
+ FizzBuzz fizzbuzz = new FizzBuzz();
+ //実行と検証
+ assertEquals("2", fizzbuzz.convert(2));
+ }
}
こちらの方が、どこで落ちたのかすぐに判別できるので、こちらの方が望ましいです。
また、assertが一つだけということで、何を検証すべきが明確になりやすく、保守しやすいコードと呼べます。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
}
ということで、こちらのコードでテストを実行します。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| +-- 1を渡すと文字列1を返す [OK]
| '-- 2を渡すと文字列2を返す [X] expected: <2> but was: <1>
'-- JUnit Vintage [OK]
「1を渡すと文字列1を返す」は成功。そうですね、テスト対象をいじってないので、期待通りです。
「2を渡すと文字列2を返す」は失敗。テスト対象をいじってないので、こちらも期待通りです。
Green:三角測量の実践
(再掲)
「三角測量」は個人的理解を言えば、データバリエーションを2つ用意し(点1・点2)、この点1・点2に基づいて、目指したい状態(点3)を実現するやり方です。
単一のテストだけでは不安がある時に用いるテクニックです。
今回は一番最初の実装ということもあり、テストが正しく実装できているのか不安ですので、このテクニックを用いてみましょう。
テストを増やすことによって、点1・点2が用意できました。
このテストコードの2つのassertを成功させるという目標を明確にすることで、実装のゴールが非常に明確になりました。
あるべき実装に変えていきましょう。
実際の実装ではこのタイミングであるべき実装を色々調べてみることになるでしょう。
その結果、数値を文字列に変換するString.valueOf()
に行き着いたとします。
実際にコードに落としてみましょう。
public class FizzBuzz {
public String convert(int i) {
- return "1";
+ return String.valueOf(i);
}
}
public class FizzBuzz {
public String convert(int i) {
return String.valueOf(i);
}
}
テストを実行しましょう。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| +-- 1を渡すと文字列1を返す [OK]
| '-- 2を渡すと文字列2を返す [OK]
'-- JUnit Vintage [OK]
無事、全てのテストに合格しました!
同時に三角測量というテクニックも身につけることができました!
Refactor
Red→Greenと来たので次はリファクタリングです。
変数名は大事なので、意味のある名前に置き換えましょう。
i
をnum
に変えましょう。(もっと良い名前もあると思いますが、勘弁してください)
public class FizzBuzz {
- public String convert(int i) {
- return String.valueOf(i);
- }
+ public String convert(int num) {
+ return String.valueOf(num);
+ }
}
テスト実行して正しいままであることを確認します。(コンソールすら割愛)
2つ目のサイクルのまとめ
テクニックについて
- 三角測量:二つのテストコードを用いて、仮実装をあるべき実装に変えていくやり方。テストに不安を感じる時に使うテクニック。
現時点のコードも改めて載せておきます。
public class FizzBuzz {
public String convert(int num) {
return String.valueOf(num);
}
}
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
}
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す
- [x] 2を渡すと文字列"2"を返す
- [ ] 3の倍数の時は数の代わりに「Fizz」に変換する
- [ ] 5の倍数の時は数の代わりに「Buzz」に変換する
- [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する
テスト容易性:低 重要度:低
- [ ] 1からnまで
- [ ] 1から100まで
- [ ] プリントする
3つ目のサイクルを回す
TODOリストの確認
次のTODOリストを選択しましょう。
- [ ] 3の倍数の時は数の代わりに「Fizz」に変換する
初回と同じように、もう少し具体性を上げて記載しましょう。
- [ ] 3の倍数の時は数の代わりに「Fizz」に変換する
- [ ] 3を渡すと文字列「Fizz」を返す
Red
テストコードを書きます。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
+
+ @Test
+ void _3を渡すと文字列Fizzを返す() {
+ //準備
+ FizzBuzz fizzbuzz = new FizzBuzz();
+ //実行と検証
+ assertEquals("Fizz", fizzbuzz.convert(3));
+ }
}
リファクタリングの基準で「3アウト制度」があります。(2アウト制度もあります)
その観点からするとリファクタリングしたくなりますが、ここではまだしません。
リファクタリングは元の状態が壊れていないことを確認しながら行うのが望ましいやり方です。
現時点ではこの3つ目のテストは失敗してしまいます。
「壊れていないことを確認しながら」というのができない状態にあります。
なので、まずはこの3つ目のテストをグリーンに持っていきます。
Green
まずは最短でグリーンにします。綺麗に書くこと(=リファクタリング)は後回しです。
public class FizzBuzz {
public String convert(int num) {
+ if (num == 3) return "Fizz";
return String.valueOf(num);
}
}
テストを動かし、成功を確認します。
:stdout:
.
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| +-- 3を渡すと文字列Fizzを返す [OK]
| +-- 1を渡すと文字列1を返す [OK]
| '-- 2を渡すと文字列2を返す [OK]
'-- JUnit Vintage [OK]
(省略)
現時点のコードはこうなっています。
public class FizzBuzz {
public String convert(int num) {
if (num == 3) return "Fizz";
return String.valueOf(num);
}
}
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
Refactor
テストコード:3アウト制
3アウト制によるリファクタリング
現状テストは全て成功で終わる状態です。
つまり「壊れていないことを確認しながら修正できる=リファクタリングできる」という条件が整いました。
ということで、3アウトしているテストコードをリファクタリングしていきましょう。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
「準備」の共通化
「準備」フェーズは重複しやすいため、テストツールはカバーしてくれてることが多いです。
jUnitだと@BeforeEach
アノテーションが該当します。
実際に書いてみましょう。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
+ @BeforeEach
+ void 前準備() {
+ }
@Test
void _1を渡すと文字列1を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
スキルのある人はいきなり「準備」の中身を実装できるでしょう。
ただ、そうじゃない人もいるし、スキルのある人でも覚え違いでとんでもない泥沼にハマってしまうこともあるでしょう。
ということで今回は、小さくグリーンをキープしたまま移行するやり方を考えます。
小さいステップでグリーンをキープしたままリファクタリングする
「準備」を共通化するということは、「実行と検証」はそれぞれに残るということです。
そうなると、共通化すべきポイントは「fizzbuzz」という変数になります。
そのためには、「fizzbuzz」をメンバ変数に格上げする必要がありそうです。
IDEの機能を使えば簡単に作れます。t-wadaさんの動画で確認しましょう。(1:48:06〜1:48:56)
またしてもCyber-dojoではやってくれないので、手動でやってみましょう。
(以下は、IDEが自動でやってくれる範囲の変更です。)
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
+ private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
}
@Test
void _1を渡すと文字列1を返す() {
//準備
- FizzBuzz fizzbuzz = new FizzBuzz();
+ fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
ここでテストしても、テストは失敗しません。安全にリファクタリングができています。
「fizzbuzz」はメンバ変数になったので、「前準備()」でインスタンス化しても良さそうな気がします。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
+ fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
//準備
- fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
ここでテストしても、テストは失敗しません。これも安全にリファクタリングができています。
現時点の状態を以下に示します。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
//準備
//実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
//準備
FizzBuzz fizzbuzz = new FizzBuzz();
//実行と検証
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
残りのメソッドへの適用は簡単ですね。
ついでに不要なコメントも削除しましょう。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
- //準備
- //実行と検証
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
- //準備
- FizzBuzz fizzbuzz = new FizzBuzz();
- //実行と検証
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
- //準備
- FizzBuzz fizzbuzz = new FizzBuzz();
- //実行と検証
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
最終的にはこんな感じ。だいぶスッキリしましたね。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
テスト対象:歩幅の調整
規約に合わせる
「歩幅の調整」に入る前に、目についたところがあるので、先にそちらをリファクタリングします。
if文には{}
が必要みたいなコーディング規約があったとするなら、そういった規約に合わせた修正もリファクタリングに含まれます。
public class FizzBuzz {
public String convert(int num) {
- if (num == 3) return "Fizz";
+ if (num == 3) {
+ return "Fizz";
+ }
return String.valueOf(num);
}
}
public class FizzBuzz {
public String convert(int num) {
if (num == 3) {
return "Fizz";
}
return String.valueOf(num);
}
}
歩幅の調整:三角測量を経由しない実装
現在はこんな感じです。
public class FizzBuzz {
public String convert(int num) {
if (num == 3) {
return "Fizz";
}
return String.valueOf(num);
}
}
前回、「仮実装」から「あるべき実装」にするために「三角測量」を使って、丁寧にやりました。
ただ、「三角測量」は「単一のテストだけでは不安がある時に用いるテクニック」です。
現時点において、テストには不安はありません。実装イメージも湧いています。
わざわざ三角測量を適用するのは、ただ周り道にしか感じません。
もちろん、まだ不安な人は「三角測量」を用いても構いません。
このように開発者のスキルに合わせて使うテクニックを変えることを、TDDでは「歩幅の調整」と言います。
テスト駆動開発は開発者の「不安」に寄り添う開発テクニックです。
不安に感じるくらいなら、使えるものは使って、安心して開発するべきです。
今回は「三角測量」は使いません。なぜなら私は不安が無いからです。
「%(剰余)」を使えば、簡単に「3の倍数」を実現できそうなので、実装してみましょう。
public class FizzBuzz {
public String convert(int num) {
- if (num == 3) {
+ if (num % 3 == 0) {
return "Fizz";
}
return String.valueOf(num);
}
}
テストを動かしましょう。
成功しますね。完璧です。
public class FizzBuzz {
public String convert(int num) {
if (num % 3 == 0) {
return "Fizz";
}
return String.valueOf(num);
}
}
3つ目のサイクルのまとめ
テクニックについて
- 3アウト制:リファクタリングの基準。3回同じコードを書いたらリファクタリングする。(人によっては2アウト制の人も居る)
- グリーンを維持したままリファクタリングする:レッドの状態でリファクタリングしてはいけない。
- 小さくリファクタリングする:バグの混入は大きく修正したときに発生する。グリーンを維持したままリファクタリングするために、1つ1つの手順は小さくする。
- 三角測量は必要に応じて使う:テストに不安を感じるときに使うテクニック。毎回使う必要はない。
現時点のコードも改めて載せておきます。
public class FizzBuzz {
public String convert(int num) {
if (num % 3 == 0) {
return "Fizz";
}
return String.valueOf(num);
}
}
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す
- [x] 2を渡すと文字列"2"を返す
- [x] 3の倍数の時は数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列「Fizz」を返す
- [ ] 5の倍数の時は数の代わりに「Buzz」に変換する
- [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する
テスト容易性:低 重要度:低
- [ ] 1からnまで
- [ ] 1から100まで
- [ ] プリントする
4つ目のサイクルを回す
TODOリストの確認
次のTODOリストを選択しましょう。
- [ ] 5の倍数の時は数の代わりに「Buzz」に変換する
前回と同じように、もう少し具体性を上げて記載しましょう。
- [ ] 5の倍数の時は数の代わりに「Buzz」に変換する
- [ ] 5を渡すと文字列「Buzz」を返す
Red
テストコードを書きます。特にいうことはないですね。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
+ @Test
+ void _5を渡すと文字列Buzzを返す() {
+ assertEquals("Buzz", fizzbuzz.convert(5));
+ }
}
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
Green:明白な実装
明白な実装
さぁ「3」を実装した時のようにif (num == 5) return "Buzz";
と書いてもいいのですが、どう考えても回り道です。
3を5に変えれば、簡単に実装できそうなのに、わざわざ遠回りをする必要性を感じません。
なぜ必要性を感じないかというと、どう実装すればいいかは明白で、テストにも不安が無いからです。
それならばいきなり最終的な形を実装してもいいでしょう。
不安が無いなら、一足飛びで綺麗な実装をしても良いのです。
これをTDDでは「明白な実装」というテクニックと呼びます。
public class FizzBuzz {
public String convert(int num) {
if (num % 3 == 0) {
return "Fizz";
}
+ if (num % 5 == 0) {
+ return "Buzz";
+ }
return String.valueOf(num);
}
}
public class FizzBuzz {
public String convert(int num) {
if (num % 3 == 0) {
return "Fizz";
}
if (num % 5 == 0) {
return "Buzz";
}
return String.valueOf(num);
}
}
テストを実行しましょう。成功します。
ただ、もしここで失敗してしまったのなら、少し謙虚になる必要がありそうです。
(テストコードが"3"のままだった、テスト対象コードが"Fizz"のままだったなど。)
ただ、もしそうだとしても修正すべき箇所は明確です。追加したコードは数行程度なのですから。
Refactor
「明白な実装」をしたこともあり、今のところ、リファクタリングするポイントは見当たらなさそうです。
今回は不要でしょう。
誤解しないで欲しいのは「Refactor不要で終わるケースは稀」というスタンスで居てほしいということです。
Refactorは、TDDの効力で非常に大きな部分です。「RefactorのためにTDDがある」と言っても過言ではないほどです。
実を言うとRefactorを後回しにするようになったらそのPJはもうダメです。突然こんなこと言ってごめんね。2
でも本当です。2、3日後には、一生着手されないリファクタチケットが複数生まれます。
それが終わりの合図です。程なく、テストすら書かれなくなるので気をつけて。
そして開発の中心メンバーが離任したら、少しだけ間をおいて終わりがきます。
4つ目のサイクルのまとめ
テクニックについて
- 明白な実装:テストに不安がなく、実装内容も明白な時は、「仮実装」を経由せずに、「あるべき実装」をしても良い。
- Refactorを後回しにしてはいけない:「終わり」が来る。
現時点のコードも改めて載せておきます。
public class FizzBuzz {
public String convert(int num) {
if (num % 3 == 0) {
return "Fizz";
}
if (num % 5 == 0) {
return "Buzz";
}
return String.valueOf(num);
}
}
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す
- [x] 2を渡すと文字列"2"を返す
- [x] 3の倍数の時は数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列「Fizz」を返す
- [x] 5の倍数の時は数の代わりに「Buzz」に変換する
- [x] 5を渡すと文字列「Buzz」を返す
- [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する
テスト容易性:低 重要度:低
- [ ] 1からnまで
- [ ] 1から100まで
- [ ] プリントする
テストの保守性
TDDのプロセスおよびテクニックは説明できたので、少し実践的な話として「テストの保守性」に触れていきます。
このコードを保守することになったとします。
もしこれを新規参画者から見るとどうなるでしょうか?
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
少なくとも、テストコードだけ見ても「仕様がわからない」ですね。
public class FizzBuzz {
public String convert(int num) {
if (num % 3 == 0) {
return "Fizz";
}
if (num % 5 == 0) {
return "Buzz";
}
return String.valueOf(num);
}
}
実装コードを見ればまだわかります。
多くの現場がこういう状況にあると思いますが、理想を言えば「テストコードから仕様が読み取れる」が望ましいです。
それを実践してみましょう。
仕様を読み取れるテストコードにするテクニック
テストの構造化
class定義による構造化
実際、仕様レベルの表現とはどういうものでしょう?
今回で言えば仕様レベルの表現は「todoリスト」に残されています。
テスト容易性:高 重要度:高
- [x] 数を文字列に変換する
- [x] 1を渡すと文字列"1"を返す
- [x] 2を渡すと文字列"2"を返す
- [x] 3の倍数の時は数の代わりに「Fizz」に変換する
- [x] 3を渡すと文字列「Fizz」を返す
- [x] 5の倍数の時は数の代わりに「Buzz」に変換する
- [x] 5を渡すと文字列「Buzz」を返す
- [ ] 3と5両方の倍数の時は数の代わりに「Buzz」に変換する
テスト容易性:低 重要度:低
- [ ] 1からnまで
- [ ] 1から100まで
- [ ] プリントする
具体的にいうと、
テストコードには3を渡すと文字列「Fizz」を返す
は記載されていますが、
それが目指すところである3の倍数の時は数の代わりに「Fizz」に変換する
という表現(仕様レベルの表現)が記載されていません。
でも、テストコードに記載れたメソッド自体は「3を渡すと文字列「Fizz」を返す」で嘘はありません。むしろ適切な命名です。
じゃぁどうすれば・・・ということで、以下に良い感じの書き方を示します。
@Nested
と class
を使います。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
+ @Nested
+ class _3の倍数の時は数の代わりにFizzに変換する {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
+ }
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
と書きたいところなんですが、Cyber-dojoではクラス名に日本語を使うとバグります。
通常の環境では日本語使えるんですが、ここは環境制約ですね・・・
ということで、Cyber-dojoに限り、以下のように書きましょうか。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
@Nested
- class _3の倍数の時は数の代わりにFizzに変換する {
+ class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
原文をそのまま持ってくる感じにしました。
テストを実行してみるとメッセージが変わっています。
確認して欲しいので、現時点のテストコードを置いておきますね。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
@Nested
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
テストを実行すると、こんな感じでメッセージの出力が変わります。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| +-- 1を渡すと文字列1を返す [OK]
| +-- 2を渡すと文字列2を返す [OK]
| +-- 3を渡すと文字列Fizzを返す [OK]
| '-- 5を渡すと文字列Buzzを返す [OK]
'-- JUnit Vintage [OK]
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| +-- 1を渡すと文字列1を返す [OK]
| +-- 2を渡すと文字列2を返す [OK]
| +-- multiples of THREE print Fizz instead of the number [OK]
| | '-- 3を渡すと文字列Fizzを返す [OK]
| '-- 5を渡すと文字列Buzzを返す [OK]
'-- JUnit Vintage [OK]
構造化されているので、わかりやすいですね。
環境制約で英文になってますが、これが日本語なら、テストコードどころかテスト結果だけでも「仕様」がわかるでしょう。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| +-- 1を渡すと文字列1を返す [OK]
| +-- 2を渡すと文字列2を返す [OK]
| +-- 3の倍数の時は数の代わりにFizzに変換する [OK]
| | '-- 3を渡すと文字列Fizzを返す [OK]
| '-- 5を渡すと文字列Buzzを返す [OK]
'-- JUnit Vintage [OK]
このまま他のにも適用していきます。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
+ @Nested
+ class convert_number_to_string {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
+ }
@Nested
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
+ @Nested
+ class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
+ }
}
テストの実行結果を確認して欲しいので、現時点のコードを置いておきます。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Nested
class convert_number_to_string {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
}
@Nested
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Nested
class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
}
テストを実行して、コンソール出力を確認してみましょう。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| +-- multiples of FIVE print Fizz instead of the number [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| +-- multiples of THREE print Fizz instead of the number [OK]
| | '-- 3を渡すと文字列Fizzを返す [OK]
| '-- convert number to string [OK]
| +-- 1を渡すと文字列1を返す [OK]
| '-- 2を渡すと文字列2を返す [OK]
'-- JUnit Vintage [OK]
日本語で実装できていたならこんな感じで出力されることでしょう。(Cyber-dojoではなく、通常の現場の開発ならこれができるはずです。)
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| +-- 5の倍数の時は数の代わりにBuzzに変換する [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| +-- 3の倍数の時は数の代わりにFizzに変換する [OK]
| | '-- 3を渡すと文字列Fizzを返す [OK]
| '-- 数を文字列に変換する [OK]
| +-- 1を渡すと文字列1を返す [OK]
| '-- 2を渡すと文字列2を返す [OK]
'-- JUnit Vintage [OK]
最初の頃よりもグッと理解しやすいですね。
適切な構造に見直す
今は、最初からコードを書いているので、違和感は無いですが、初めてテストコードをみると少し違和感を覚えることでしょう。
わかりやすくするために、日本語バージョンの出力で考えてみましょう。
| +-- 5の倍数の時は数の代わりにBuzzに変換する [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| +-- 3の倍数の時は数の代わりにFizzに変換する [OK]
| | '-- 3を渡すと文字列Fizzを返す [OK]
| '-- 数を文字列に変換する [OK]
| +-- 1を渡すと文字列1を返す [OK]
| '-- 2を渡すと文字列2を返す [OK]
引っ掛かるのは「数を文字列に変換する」です。
3の時も5の時も、数を文字列に変換しています。
ということは、この表現では仕様の説明として適切な表現になっていません。
「数を文字列に変換する」は、全ての親だとするのが適切でしょう。
| '-- 数を文字列に変換する [OK]
| +-- 5の倍数の時は数の代わりにBuzzに変換する [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| +-- 3の倍数の時は数の代わりにFizzに変換する [OK]
| | '-- 3を渡すと文字列Fizzを返す [OK]
| '-- ???????????????????????????????? [OK]
| +-- 1を渡すと文字列1を返す [OK]
| '-- 2を渡すと文字列2を返す [OK]
そうすると1を渡すと文字列1を返す
と2を渡すと文字列2を返す
に適切なグループ名が必要そうです。
他のテストと並べて考えるとその他の数の時はそのまま文字列に変換する
あたりが良さそうです。
ということで、それを適用するとこんな感じを目指した方が良さそうです。
| '-- 数を文字列に変換する [OK]
| +-- 5の倍数の時は数の代わりにBuzzに変換する [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| +-- 3の倍数の時は数の代わりにFizzに変換する [OK]
| | '-- 3を渡すと文字列Fizzを返す [OK]
| '-- その他の数の時はそのまま文字列に変換する [OK]
| +-- 1を渡すと文字列1を返す [OK]
| '-- 2を渡すと文字列2を返す [OK]
これを見て、さらに思いつきました。
これらのテストは「convertメソッド」を対象にしたテストです。この表現を追加した方がわかりやすそうです。
数を文字列に変換する
ではなくconvertメソッドは数を文字列に変換する
にしてしまいましょう。
ということで、それを適用するとこんな感じを目指しましょう。
| '-- convertメソッドは数を文字列に変換する [OK]
| +-- 5の倍数の時は数の代わりにBuzzに変換する [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| +-- 3の倍数の時は数の代わりにFizzに変換する [OK]
| | '-- 3を渡すと文字列Fizzを返す [OK]
| '-- その他の数の時はそのまま文字列に変換する [OK]
| +-- 1を渡すと文字列1を返す [OK]
| '-- 2を渡すと文字列2を返す [OK]
実際に実装してみましょう。
わかりやすくするために、場所の入れ替えが発生していますので、ご注意ください。
また、Cyber-dojo制約でクラス名が英語になってる点はご容赦ください。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
+ @Nested
+ class convert_method_convert_number_to_string {
@Nested
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Nested
class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
@Nested
- class convert_number_to_string {
+ class OTHERS_print_number {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
}
+ }
}
ということで、以下のようになるはずです。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Nested
class convert_method_convert_number_to_string {
@Nested
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Nested
class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
@Nested
class OTHERS_print_number {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
}
}
}
実際にテストを実施してみましょう。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK]
| '-- convert method convert number to string [OK]
| +-- OTHERS print number [OK]
| | +-- 1を渡すと文字列1を返す [OK]
| | '-- 2を渡すと文字列2を返す [OK]
| +-- multiples of FIVE print Fizz instead of the number [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| '-- multiples of THREE print Fizz instead of the number [OK]
| '-- 3を渡すと文字列Fizzを返す [OK]
'-- JUnit Vintage [OK]
出力の順序や英語なところは多少気に食わないですが、期待する構造化はできていますね。
仕様書としてもっとわかりやすくする
今の状態でも十分わかりやすくなりましたが、我々が目指すところは「テストコードを仕様書として扱う」ところです。
要は、このテストコードはもっとわかりやすくできます。
具体的に言えば、以下の部分を日本語表現することができます。
+-- JUnit Jupiter [OK]
| '-- FizzBuzzTest [OK] <------------------------------- ここが直せるよ!
| '-- convert method convert number to string [OK]
| +-- OTHERS print number [OK]
| | +-- 1を渡すと文字列1を返す [OK]
| | '-- 2を渡すと文字列2を返す [OK]
| +-- multiples of FIVE print Fizz instead of the number [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| '-- multiples of THREE print Fizz instead of the number [OK]
| '-- 3を渡すと文字列Fizzを返す [OK]
'-- JUnit Vintage [OK]
どうやるかというと、@DisplayName
アノテーションを用います。
実際に書いてみましょう。
1行追加するだけです。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
+@DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス")
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Nested
class convert_method_convert_number_to_string {
@Nested
@DisplayName("3の倍数の時は数の代わりにFizzに変換する")
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Nested
class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
@Nested
class OTHERS_print_number {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
}
}
}
実際にテストを動かしてみましょう。
+-- JUnit Jupiter [OK]
| '-- Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス [OK]
| '-- convertメソッドは数を文字列に変換する [OK]
| +-- OTHERS print number [OK]
| | +-- 1を渡すと文字列1を返す [OK]
| | '-- 2を渡すと文字列2を返す [OK]
| +-- multiples of FIVE print Fizz instead of the number [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| '-- multiples of THREE print Fizz instead of the number [OK]
| '-- 3を渡すと文字列Fizzを返す [OK]
'-- JUnit Vintage [OK]
良い感じですね。変わりました。
ここで、一つ気づきました。これが使えるなら、Cyber-dojo環境でもテスト結果が見やすくできそうですね。
サブクラス側にも適用してみましょう。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス")
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Nested
+ @DisplayName("convertメソッドは数を文字列に変換する")
class convert_method_convert_number_to_string {
@Nested
+ @DisplayName("3の倍数の時は数の代わりにFizzに変換する")
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Nested
+ @DisplayName("5の倍数の時は数の代わりにBuzzに変換する")
class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
@Nested
+ @DisplayName("その他の数の時はそのまま文字列に変換する")
class OTHERS_print_number {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
}
}
}
ということで最終形は以下な感じです。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス")
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Nested
@DisplayName("convertメソッドは数を文字列に変換する")
class convert_method_convert_number_to_string {
@Nested
@DisplayName("3の倍数の時は数の代わりにFizzに変換する")
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Nested
@DisplayName("5の倍数の時は数の代わりにBuzzに変換する")
class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
@Nested
@DisplayName("その他の数の時はそのまま文字列に変換する")
class OTHERS_print_number {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
@Test
void _2を渡すと文字列2を返す() {
assertEquals("2", fizzbuzz.convert(2));
}
}
}
}
テストを実行してみましょう。
+-- JUnit Jupiter [OK]
| '-- Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス [OK]
| '-- convertメソッドは数を文字列に変換する [OK]
| +-- その他の数の時はそのまま文字列に変換する [OK]
| | +-- 1を渡すと文字列1を返す [OK]
| | '-- 2を渡すと文字列2を返す [OK]
| +-- 5の倍数の時は数の代わりにBuzzに変換する [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| '-- 3の倍数の時は数の代わりにFizzに変換する [OK]
| '-- 3を渡すと文字列Fizzを返す [OK]
'-- JUnit Vintage [OK]
サイコーですね。完璧です。
とてもわかりやすいですね。
テストのメンテナンスコストに思いを馳せる
不要なテストを消す
テストを構造化してみると、少し気づくことがあります。
「3の倍数の時は数の代わりにFizzに変換する」は「1つ」のテスト。
「5の倍数の時は数の代わりにBuzzに変換する」は「1つ」のテスト。
「その他の数の時はそのまま文字列に変換する」は 「2つ」 のテスト。
+-- JUnit Jupiter [OK]
| '-- Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス [OK]
| '-- convertメソッドは数を文字列に変換する [OK]
| +-- その他の数の時はそのまま文字列に変換する [OK] <------- この分類だけテストが 2つ ある!!!
| | +-- 1を渡すと文字列1を返す [OK]
| | '-- 2を渡すと文字列2を返す [OK]
| +-- 5の倍数の時は数の代わりにBuzzに変換する [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| '-- 3の倍数の時は数の代わりにFizzに変換する [OK]
| '-- 3を渡すと文字列Fizzを返す [OK]
'-- JUnit Vintage [OK]
新規参画者がこのコードを見た場合、きっと何か意味があって「2つ」にしたのだろうと考えるでしょう。
きっと「PJルールかな?」とか考えたりするでしょう。でも探しても答えは見つかりません。
実装時に三角測量のためだけに作ったテストですから、きっとわざわざドキュメントとして情報を残してたりはしないでしょう。
私が新規参画者だとするなら、答えが見つからない時は、 前任者に倣って 全てのテストを2つずつにしてしまうでしょう。
不安な場合は安全そうな方に倒したくなります。(そのときに論理はありません。不安な感情の圧力は凄まじい。)
ただ、全てを知ってる側からすると、これは無駄に見えます。そんなことをしてしまっては、無駄なコードが増えます。
でも、全てを知ることはできません。途中から来た人は、歴史的背景なんてわかりません。目の間のことが全てです。
では、誰がこの無駄なコードの増殖を止めれるでしょうか?
最初にこのテストコードを書いた人だけです。
このテストを実装した人間だけが、ただ三角測量という「実装時の不安」のためだけに増やしたテストだと知っています。
このテストを実装した人間だけが、「その他の数の時はそのまま文字列に変換する」を機能を検証するだけならテストは1つだけで良いことを知っています。
テストを実装した人がテストのメンテナンスを意識しなかった場合、以下のようにテストのメンテナンスコストは膨れていきます。
ということで、テストのメンテナンスコストを抑制するため、不要なテストは削除しましょう。
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス")
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Nested
@DisplayName("convertメソッドは数を文字列に変換する")
class convert_method_convert_number_to_string {
@Nested
@DisplayName("3の倍数の時は数の代わりにFizzに変換する")
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Nested
@DisplayName("5の倍数の時は数の代わりにBuzzに変換する")
class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
@Nested
@DisplayName("その他の数の時はそのまま文字列に変換する")
class OTHERS_print_number {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
- @Test
- void _2を渡すと文字列2を返す() {
- assertEquals("2", fizzbuzz.convert(2));
- }
}
}
}
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス")
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Nested
@DisplayName("convertメソッドは数を文字列に変換する")
class convert_method_convert_number_to_string {
@Nested
@DisplayName("3の倍数の時は数の代わりにFizzに変換する")
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Nested
@DisplayName("5の倍数の時は数の代わりにBuzzに変換する")
class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
@Nested
@DisplayName("その他の数の時はそのまま文字列に変換する")
class OTHERS_print_number {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
}
}
}
テストを実行したらこんな感じになります。
+-- JUnit Jupiter [OK]
| '-- Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス [OK]
| '-- convertメソッドは数を文字列に変換する [OK]
| +-- その他の数の時はそのまま文字列に変換する [OK]
| | '-- 1を渡すと文字列1を返す [OK]
| +-- 5の倍数の時は数の代わりにBuzzに変換する [OK]
| | '-- 5を渡すと文字列Buzzを返す [OK]
| '-- 3の倍数の時は数の代わりにFizzに変換する [OK]
| '-- 3を渡すと文字列Fizzを返す [OK]
'-- JUnit Vintage [OK]
個人的な補足:テストの要否
ここはt-wadaさん関係なく、個人的な補足です。
TDDを実践適用しようとすると、テストコードに求めるものが増えてくるように感じます。
(「テストコードの役割をようやく認識できた」っていう方が適切な表現かもしれないですが)
- 開発者の実装を支援するガードレール(TDDのプロセスでテストコードに期待しているのは主にこの役割)
- 仕様を伝えるドキュメント(発展系としてBDDに繋がる考え方)
- 品質保証としてのテスト(TDDの文脈でも触れられてはいるんだけど、実感としては源流が異なる認識)
TDDの観点(ガードレールやドキュメントという役割)からすると、「2を渡すと文字列2を返す」は「不要」という判断で良いと思っています。
ただし「品質保証」という観点からするとPJごとによって判断が変わってくると思っています。
例えば、FizzBuzzに「境界値分析」の考え方を適用したとき、以下のようなケースでテストをしよう、という話になることもあると思います。
ケースの作成観点
- 数字・Fizz・Buzz・FizzBuzzは、最初と最後の出力は確認しよう。
- 倍数での出力が機能しているか確認したい。
- Fizz・Buzz・FizzBuzzで、それぞれ1倍のケースを検証しよう。
- Fizz・Buzz・FizzBuzzで、2倍・3倍・4倍のケースを散らして検証しよう。
- 境界値の検証をしよう。
- 境界値は以下整理としよう。
- 数字/Fizzの入り口/出口(数字・Fizz・数字の3ケース)
- 数字/Buzzの入り口/出口(数字・Buzz・数字の3ケース)
- 数字/FizzBuzzの入り口/出口(数字・FizzBuzz・数字の3ケース)
- 境界値は最初と最後の両方を見るとちょっと多いので、最初の境界値だけ検証しよう。
実際に検証される数字(16個)
(「2」がテスト対象になることもあるよって言いたいだけなので、数字順に説明する書き方をしてます。)
- 1は最初の数字だからやろう。
- 2は最初の境界値(数字/Fizzの数字側入口)だからやろう。
- 3は最初のFizzだからやろう。
- 4は最初の境界値(数字/Fizzの数字側出口・数字/Buzzの数字側入口)だからやろう。
- 5は最初のBuzzだからやろう。
- 6は「3の倍数(*2)」であることを確認するためにやろう。
- 15は「5の倍数(*3)」であることを確認するためにやろう。
- 11は最初の境界値(数字/Buzzの数字側出口)だからやろう。
- 14は最初の境界値(数字/FizzBuzzの数字側入口)だからやろう。
- 15は最初のFizzBuzzだからやろう。
- 16は最初の境界値(数字/FizzBuzzの数字側出口)だからやろう。
- 60は「15の倍数(*4)」であることを確認するためにやろう。
- 90は最後の「FizzBuzz」だからやろう。
- 98は最後の「数字」だからやろう。
- 99は最後の「Fizz」だからやろう。
- 100は最後の「Buzz」だからやろう。
みたいな。
正直「FizzBuzzごときに、ここまではやりすぎでは?」と思いますが、金融系などのお金の計算処理などではもしかしたらもっと徹底してテストを書くことになるかもしれません。
そういった場合においては、「2を渡すと文字列2を返す」が「必要」と判断されるケースもあると思っています。
このように、品質保証の観点からすると、PJによって要否の基準って変わると思うので、テストのメンテナンスにどう向き合っていくのかは、PJごとに考えるべきだと思っています。
ここらへん、いろんな考え・視点あると思うので、ぜひコメントいただけたらと思います。(「そう思うよ!」とかだけでも嬉しい。)
(QAエンジニアの方からすると、きっとツッコミどこ満載だろうなって思いながら書いてる)
ちなみに、以下書籍をよく読むとここら辺に関するケントベックの考えが記載されているので、興味ある方はぜひ読んでみるのが良いと思います。
テストの保守性についてのまとめ
- テストを構造化しよう:jUnitでは「
class
と@Nested
」を使うと構造化できる。 - テストを仕様書として使えるレベルにする:
@DisplayName
の活用はもちろん、構造全体として意味の通じるものになっているかも確認しよう。 - 不要なテストは削除しよう:少なくとも「三角測量」のためだけのテストは不要です。減らしましょう。
- 不要なテストはPJによって異なる:何を「不要」とするかは、PJによって基準が異なります。周りのメンバーと議論するのが良いでしょう。(記事作成者の私見)
総まとめ
FizzBuzz問題の全てを実装したわけではないですが、目的だった「TDDを習得すること」は達成できているのではないでしょうか?
この記事を最後まで実施した場合のコードと、各章でまとめた内容を全部持ってきましたので、おさらいして終わりにしましょう。
コード
最終的にはこんな感じです。
public class FizzBuzz {
public String convert(int num) {
if (num % 3 == 0) {
return "Fizz";
}
if (num % 5 == 0) {
return "Buzz";
}
return String.valueOf(num);
}
}
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayName("Fizz Buzz 数列と変換規則を扱う FizzBuzz クラス")
public class FizzBuzzTest {
private FizzBuzz fizzbuzz;
@BeforeEach
void 前準備() {
fizzbuzz = new FizzBuzz();
}
@Nested
@DisplayName("convertメソッドは数を文字列に変換する")
class convert_method_convert_number_to_string {
@Nested
@DisplayName("3の倍数の時は数の代わりにFizzに変換する")
class multiples_of_THREE_print_Fizz_instead_of_the_number {
@Test
void _3を渡すと文字列Fizzを返す() {
assertEquals("Fizz", fizzbuzz.convert(3));
}
}
@Nested
@DisplayName("5の倍数の時は数の代わりにBuzzに変換する")
class multiples_of_FIVE_print_Fizz_instead_of_the_number {
@Test
void _5を渡すと文字列Buzzを返す() {
assertEquals("Buzz", fizzbuzz.convert(5));
}
}
@Nested
@DisplayName("その他の数の時はそのまま文字列に変換する")
class OTHERS_print_number {
@Test
void _1を渡すと文字列1を返す() {
assertEquals("1", fizzbuzz.convert(1));
}
}
}
}
今までのまとめ
最初のサイクルについて
- 最初のサイクルは、非常にやることが多い。
- テストクラスの作成
- テストツールは正しく動くのか?
- テスト対象クラスの作成
- だからこそ、テスト容易性の高いTODOを選ぶことが大事。
- 副作用としてひどいコード
return "1";
ができるが、それは以降のTODOで解消していく。
テクニックについて
- 基本となるテクニック
- 最初のテストは必ず失敗するテストを書く:jUnitが動くことを確実に確認するために
fail("メッセージ")
を使う。 - テストは下から書く:下から書くことで、実装目的を明確にできるし、IDEの機能のおかげで効率よく実装できる。
- TDDはペアプロ・モブプロに特に効く:テストを下から書くことも合わさり、実装目的が非常に明確になるため、ペアプロ・モブプロ時の半離脱状態を防げる。
- 仮実装:
return "1";
といったようなテストを通す最小限の実装を指す。テストコードのテストという意味合いもある。
- 最初のテストは必ず失敗するテストを書く:jUnitが動くことを確実に確認するために
- 不安がある時の実装方法
- 三角測量:二つのテストコードを用いて、仮実装をあるべき実装に変えていくやり方。テストに不安を感じる時に使うテクニック。
- 三角測量は必要に応じて使う:テストに不安を感じるときに使うテクニック。毎回使う必要はない。
- 不安がない時の実装方法
- 明白な実装:テストに不安がなく、実装内容も明白な時は、「仮実装」を経由せずに、「あるべき実装」をしても良い。
- リファクタリングに関するテクニック
- jUnitを何度も回しながらリファクタリングする:バグ混入タイミングで検知できるため、修正が容易。TDDによる非常に大きな恩恵の一つ。
- 3アウト制:リファクタリングの基準。3回同じコードを書いたらリファクタリングする。(人によっては2アウト制の人も居る)
- グリーンを維持したままリファクタリングする:レッドの状態でリファクタリングしてはいけない。
- 小さくリファクタリングする:バグの混入は大きく修正したときに発生する。グリーンを維持したままリファクタリングするために、1つ1つの手順は小さくする。
- Refactorを後回しにしてはいけない:「終わり」が来る。
テストの保守性について
- テストを構造化しよう:jUnitでは「
class
と@Nested
」を使うと構造化できる。 - テストを仕様書として使えるレベルにする:
@DisplayName
の活用はもちろん、構造全体として意味の通じるものになっているかも確認しよう。 - 不要なテストは削除しよう:少なくとも「三角測量」のためだけのテストは不要です。減らしましょう。
- 不要なテストはPJによって異なる:何を「不要」とするかは、PJによって基準が異なります。周りのメンバーと議論するのが良いでしょう。(記事作成者の私見)
さいごに
改めてですが、TDDは兎にも角にも「手に馴染ませる」ことが重要なテクニックだと思います。
実践の中でやるとまた違った気づきも多く得られると思います。
ぜひいろんな場面でTDDを実践する人が増えたら良いなと思っております。
最後に改めてですが、書籍を紹介しておきます。
WEB情報(動画や記事)は手軽に入手できますが、その真髄を理解するにはあまり適していない手段だと思っています。
みんなも買おう!
以上です。
-
一応記事のオリジナリティとして、ブラウザだけで実践した個人的理解を足したりはしてるものの、本編の進め方はもうほんとにそのままなので・・・オマージュとかなんかそういう言葉で逃げられるレベルではない。。。 ↩
-
最終兵器彼女構文。この構文、自分ですらジャスト世代かどうか怪しいので、ターゲットにしてる世代に通じる構文ではないだろうな。 漫画:https://www.amazon.co.jp/dp/4091856810 「実を言うと地球はもうだめです。突然こんなこと言ってごめんね。でも本当です。2、3日後にものすごく赤い朝焼けがあります。それが終わりの合図です。程なく大きめの地震が来るので気をつけて。それがやんだら、少しだけ間をおいて終わりがきます。」 ↩