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マーケットプレイスからインストールできます。


SubversiveEclipseから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);
    }
}