JavaでCodeKata of Bowlingをやってみた
この動画をリスペクト。久々に見たのですが、前にも増して楽しかったです!
スはスペックのス〜RSpecによるテスト駆動開発の実演〜 - 角谷信太郎 (1/3)
開発環境
- Eclipse Java SEまたはEE
- JUnit4.4以上
- QuickJUnit(推奨)
- Subversive(任意)
- Vrapper(任意)
今回は、日本語 Eclipse 3.6 Pleiades All in One Javaを使用しました。これだけで日本語の開発環境が手に入ります。設定不要なのでとても楽です。
JUnitは、4.4からの新機能assertThatを使用していますが、アサーションをassertEqualsに変えてケース名をtest〜〜に変えれば3系でも問題ないです。
QuickJUnitはCtrl+9でテストクラス間切り替え、Ctrl+0でカーソル下のテストを実行できるため、テストが高速化されます。自動テストと同じくらい快適です。Eclipse3.6以上ならヘルプのEclipseマーケットプレイスからインストールできます。
SubversiveはEclipseからSubversionを使用する際の橋渡しプラグインです。今回はローカルにリポジトリを作成して、チーム=>プロジェクトの共有でfile://localhost/c:/〜〜〜/Bowlingを指定して逐一コミットしながら行いましたが、初めのうちはなくてもよいと思います。QuickJUnitと同じくEclipseマーケットプレイスからインストールできます。
Vrapperは任意ですが、ほとんど副作用なくEcllipseでVimのキーバインドを実現できるという素晴らしいプラグインです。参考:Eclipseのキーバインドをvim風にできるVrapperが素晴らしすぎる件について - ( ꒪⌓꒪) ゆるよろ日記
はじめ!
まずは、Bowlingプロジェクトを作成してGameクラスとGameTestクラスを作成します。
// Game.java public class Game { }
// GameTest.java public class GameTest { }
最初のテスト
以降は動画の手順をまるっとパクって進めます。
import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import org.junit.Test; public class GameTest { @Test public void すべてガターの場合() { Game game = new Game(); for(int i = 0; i < 20; i++) { game.roll(0); } assertThat(game.score(), is(0)); } }
Gameクラスにrollメソッドやscoreメソッドがないため、ビルドエラーになります。まずはエラーをなくしましょう。
public class Game { public void roll(int pins) { } public int score() { return -1; } }
ここでCtrl+0でテストを実行します。0を期待したのに-1が返ってくるのでRedになります。
RedになったのでCtrl+9で切り替えてコードを書きます。まずはいんちきで。
public class Game { public void roll(int pins) { } public int score() { return 0; } }
Ctrl9=>Ctrl+0でテストを実行すると、見事Greenバーが出現します。これで1ケース終了です。バージョン管理システムを使っていればここでコミットしましょう。
2つめのテスト
次はすべて1ピンの場合です。
@Test public void すべて1ピンの場合() { Game game = new Game(); for(int i = 0; i < 20; i++) { game.roll(1); } assertThat(game.score(), is(20)); }
Ctrl+0でRedになったことを確認してCtrl+9で切り替えます。
public class Game { private int score = 0; public void roll(int pins) { score += pins; } public int score() { return score; } }
Ctrl+9=>Ctrl+0でGreenバーを見ます。これで2ケース終了です。
第三のテスト
ここで次のテストを書くのですが、このままではGreenにするために大幅な修正が必要なことわかったので、いったんコメントアウトしてリファクタリングを行います。
// @Test // public void ストライクの場合() { // Game game = new Game(); // game.roll(10); //strike // game.roll(3); // game.roll(4); //24 // for(int i = 0; i < 18; i++) { // game.roll(0); // } // assertThat(game.score(), is(24)); // }
戻してリファクタリング
ここでリファクタリングを行います。リファクタリングする前とした後にCtrl+9=>Ctrl+0でバーがGreenになっていることを確認しましょう。
import java.util.*; public class Game { private int score = 0; private List<Integer> rolls = new ArrayList<Integer>(); public void roll(int pins) { rolls.add(pins); } public int score() { for (int roll : rolls) { score += roll; } return score; } }
まだGreenです。Greenなので、切りのいいところでコミットしておきます。
テスト復活
リファクタリングができたので、コメントアウトしたテストを復活させてCtrl+0で実行します。
@Test public void ストライクの場合() { Game game = new Game(); game.roll(10); //strike game.roll(3); game.roll(4); //24 for(int i = 0; i < 18; i++) { game.roll(0); } assertThat(game.score(), is(24)); }
見事Redになりました!
Redになったので、コードを書きます。
import java.util.*; public class Game { private List<Integer> rolls = new ArrayList<Integer>(); public void roll(int pins) { rolls.add(pins); } public int score() { int score = 0; int rollIdx = 0; for (int i = 0; i < 10; i++) { score += rolls.get(rollIdx) + rolls.get(rollIdx + 1); rollIdx += 2; } return score; } }
見事Greenになりました!リファクタリングによって機能追加がかんたんになっています。
パーフェクトゲームの場合
すぐに通るとおもいきや、点数計算でArrayIndexOutOfBoundsExceptionが発生してしままいました。Rubyでも同じロジックならFixnum + nilでエラーになると思うのですが、どこか間違えているでしょうか。
@Test public void パーフェクトゲームの場合() { Game game = new Game(); for(int i = 0; i < 10; i++) { game.roll(10); } assertThat(game.score(), is(300)); }
最後のゲームでストライクだった場合に対応させます。
import java.util.*; public class Game { private List<Integer> rolls = new ArrayList<Integer>(); public void roll(int pins) { rolls.add(pins); } public int score() { int score = 0; int rollIdx = 0; for (int i = 0; i < 10; i++) { if (rolls.get(rollIdx) == 10) { int next = ((rollIdx + 1) < rolls.size()) ? rolls.get(rollIdx + 1) : 0; int nextNext = ((rollIdx + 2) < rolls.size()) ? rolls.get(rollIdx + 2) : 0; score += 10 + next + nextNext; rollIdx += 1; } else { score += rolls.get(rollIdx) + rolls.get(rollIdx + 1); rollIdx += 2; } } return score; } }
テストケースのリファクタリング
ここでガターとストライクのロールをメソッド化します。
import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import org.junit.Test; public class GameTest { @Test public void すべてガターの場合() { Game game = new Game(); for(int i = 0; i < 20; i++) { rollGutter(game); } assertThat(game.score(), is(0)); } @Test public void すべて1ピンの場合() { Game game = new Game(); for(int i = 0; i < 20; i++) { game.roll(1); } assertThat(game.score(), is(20)); } @Test public void ストライクの場合() { Game game = new Game(); rollStrike(game); game.roll(3); game.roll(4); //24 for(int i = 0; i < 16; i++) { rollGutter(game); } assertThat(game.score(), is(24)); } @Test public void パーフェクトゲームの場合() { Game game = new Game(); for(int i = 0; i < 12; i++) { rollStrike(game); } assertThat(game.score(), is(300)); } private void rollGutter(Game game) { game.roll(0); } private void rollStrike(Game game) { game.roll(10); } }
まだまだGreenです。
スペアの場合
@Test public void スペアの場合() { Game game = new Game(); rollSpare(game); game.roll(4); game.roll(3); //21 for(int i = 0; i < 16; i++) { rollGutter(game); } assertThat(game.score(), is(21)); } private void rollSpare(Game game) { game.roll(5); game.roll(5); }
import java.util.*; public class Game { private List<Integer> rolls = new ArrayList<Integer>(); public void roll(int pins) { rolls.add(pins); } public int score() { int score = 0; int rollIdx = 0; for (int i = 0; i < 10; i++) { if (rolls.get(rollIdx) == 10) { int next = ((rollIdx + 1) < rolls.size()) ? rolls.get(rollIdx + 1) : 0; int nextNext = ((rollIdx + 2) < rolls.size()) ? rolls.get(rollIdx + 2) : 0; score += 10 + next + nextNext; rollIdx += 1; } else if (rolls.get(rollIdx) + rolls.get(rollIdx + 1) == 10) { int next = ((rollIdx + 2) < rolls.size()) ? rolls.get(rollIdx + 2) : 0; score += 10 + next; rollIdx += 2; } else { score += rolls.get(rollIdx) + rolls.get(rollIdx + 1); rollIdx += 2; } } return score; } }
Ctrl+9とCtrl+0だけでファイル切り替えとテスト実行できます。右クリックメニューからテスト実行を選択しなくてよいので、繊細なマウス操作が不要となり健康的ですね。
受け入れテスト
いよいよ受け入れテストを通してみます。
@Test public void 受け入れテスト() { Game game = new Game(); int[] pins = new int[] {1,4,4,5,6,4,5,5,10,0,1,7,3,6,4,10,2,8,6}; for(int pin : pins) { game.roll(pin); } assertThat(game.score(), is(133)); }
見事通りました!
最後までリファクタリング
さいごに、明後日の自分のためにリファクタリングを行って終了です。
import java.util.*; public class Game { private static final int FRAMES_OF_A_GAME = 10; private List<Integer> rolls = new ArrayList<Integer>(); public void roll(int pins) { rolls.add(pins); } public int score() { int score = 0; int rollIdx = 0; for (int i = 0; i < FRAMES_OF_A_GAME; i++) { if (isStrike(rollIdx)) { score += strikeBonus(rollIdx); rollIdx += 1; } else if (isSpare(rollIdx)) { score += spareBonus(rollIdx); rollIdx += 2; } else { score += scoreOfFrame(rollIdx); rollIdx += 2; } } return score; } private boolean isStrike(int rollIdx) { return rolls.get(rollIdx) == 10; } private boolean isSpare(int rollIdx) { return rolls.get(rollIdx) + rolls.get(rollIdx + 1) == 10; } private int scoreOfFrame(int rollIdx) { return rolls.get(rollIdx) + rolls.get(rollIdx + 1); } private int strikeBonus(int rollIdx) { int next = nextPins(rollIdx); int nextNext = nextNextPins(rollIdx); return 10 + next + nextNext; } private int spareBonus(int rollIdx) { int nextNext = nextNextPins(rollIdx); return 10 + nextNext; } private int nextPins(int rollIdx) { return ((rollIdx + 1) < rolls.size()) ? rolls.get(rollIdx + 1) : 0; } private int nextNextPins(int rollIdx) { return ((rollIdx + 2) < rolls.size()) ? rolls.get(rollIdx + 2) : 0; } }
おつかれさまでした!
追記
すべて終わってから試したところ、ArrayIndexOutOf〜〜は発生しませんでした。なので
private int nextPins(int rollIdx) { return rolls.get(rollIdx + 1); } private int nextNextPins(int rollIdx) { return rolls.get(rollIdx + 2); }
でOKです。仕組みはあとで考えよう・・。これはデジャヴ?
テストコード全容
import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import org.junit.Test; public class GameTest { @Test public void すべてガターの場合() { Game game = new Game(); for(int i = 0; i < 20; i++) { rollGutter(game); } assertThat(game.score(), is(0)); } @Test public void すべて1ピンの場合() { Game game = new Game(); for(int i = 0; i < 20; i++) { game.roll(1); } assertThat(game.score(), is(20)); } @Test public void ストライクの場合() { Game game = new Game(); rollStrike(game); game.roll(3); game.roll(4); //24 for(int i = 0; i < 16; i++) { rollGutter(game); } assertThat(game.score(), is(24)); } @Test public void パーフェクトゲームの場合() { Game game = new Game(); for(int i = 0; i < 12; i++) { rollStrike(game); } assertThat(game.score(), is(300)); } @Test public void スペアの場合() { Game game = new Game(); rollSpare(game); game.roll(4); game.roll(3); //21 for(int i = 0; i < 16; i++) { rollGutter(game); } assertThat(game.score(), is(21)); } @Test public void 受け入れテスト() { Game game = new Game(); int[] pins = new int[] {1,4,4,5,6,4,5,5,10,0,1,7,3,6,4,10,2,8,6}; for(int pin : pins) { game.roll(pin); } assertThat(game.score(), is(133)); } private void rollGutter(Game game) { game.roll(0); } private void rollStrike(Game game) { game.roll(10); } private void rollSpare(Game game) { game.roll(5); game.roll(5); } }