2章:⑭実装とテスト〜じゃんけんゲームの各部品のみのテスト〜

2-4 実装とテスト〜じゃんけんゲームの各部品のみ〜

本パートでは、テストケースの作成を行った時の前章(FirstJankenMainTest)との大きな違いについて記述します。
JUnitを使用したテストは下の動画のように行うことができます。

正直のところ、ここまで学習してきたらあとは自分の力でできてしまうと思います。

前章で作成したテストクラスの中身ですが、FirstJankenMainのメソッドがprivateなので

リフレクションを使用してテストを行いましたが、今回は各部品(JankenUtilsやConsoleUtils)に分けているのでアクセス修飾子は「public」です。
※privateは外部(クラスの外)から呼び出せないが、リフレクションを使ってメソッドのみ取り出して実行するということを行いました。

具体的に下のようなコードでリフレクションの実装を行いました。getPrivateMethod()を呼び出してテストするメソッドを取得しました。

※リフレクションはちょっと難しいので、今は「メソッドをとりだすことができるんだな」と理解しておいてください。

<リフレクションを使ったサンプルコード>

/**
 * java.lang.refrectionを使用してプライベート修飾子のメソッドを取得します。
 * ※privateは外部から参照することができないのでアクセス権を変更する必要がある。
 *  実装方法: clazz.setAccessible(true);
 *
 * @param clazz テスト対象クラス
 * @param methodName テストするメソッド名
 * @args 起動するメソッドの引数
 */
private Method getPrivateMethod(Class clazz, String methodName, Class<?> ... paramType) {
    // テスト対象クラスを返却する
    Method testMethod = null;
    try {
        testMethod = clazz.getDeclaredMethod(methodName, paramType);
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (SecurityException e) {
        e.printStackTrace();
    }
    return testMethod;
}

今度は、アクセス修飾子が「public」なのでリフレクションを使用しなくてもよい状態です。

そうすると、テストもすごく楽になります。下のコードはJankenUtilsのテストクラスです。

1. JankenUtilsのテストクラス

実施するテストするメソッド一覧

==JankenUtils==

  • createJudgeMap
  • acceptInput
  • judgeWinLose

<JankenUtilsTest>

public class JankenUtilsTest {
    /** テスト対象クラス */
    private static JankenUtils target;
    /** ログ出力 */
    private static final Logger LOG = LoggerFactory.getLogger(JankenUtilsTest.class);

    /**
     * すべてのテストケースを実行するための準備をする。
     */
    @BeforeClass
    public static void initClass() {
        target  = new JankenUtils();
    }

    /**
     * 勝敗判定MAP作成の確認
     */
    @Test
    public void testCreateJudgeMap() {
        Map<String, JankenConst> map = target.getJudgeMap();
        assertNotNull(map);
        assertEquals(YOU_WIN, map.get(GU.toString() + CHOKI.toString()));
        assertEquals(YOU_WIN, map.get(CHOKI.toString() + PA.toString()));
        assertEquals(YOU_WIN, map.get(PA.toString() + GU.toString()));
        assertEquals(YOU_LOSE, map.get(GU.toString() + PA.toString()));
        assertEquals(YOU_LOSE, map.get(CHOKI.toString() + GU.toString()));
        assertEquals(YOU_LOSE, map.get(PA.toString() + CHOKI.toString()));
        assertEquals(AIKO, map.get(GU.toString() + GU.toString()));
        assertEquals(AIKO, map.get(CHOKI.toString() + CHOKI.toString()));
        assertEquals(AIKO, map.get(PA.toString() + PA.toString()));
    }

    /**
     * 入力テスト「Hello」と入力する
     */
    @Test
    public void testAcceptInput() {
        LOG.info(() -> "*** Helloと入力するテスト ***");
        String input = target.acceptInput();
        assertEquals("Hello", input);
    }

    /**
     * 勝敗判定のテスト
     */
    @Test
    public void testjudgeWinLose() {
        assertEquals(YOU_WIN, target.judgeWinLose(GU.toString(), CHOKI.toString()));
        assertEquals(YOU_WIN, target.judgeWinLose(CHOKI.toString(), PA.toString()));
        assertEquals(YOU_WIN, target.judgeWinLose(PA.toString(), GU.toString()));
        assertEquals(YOU_LOSE, target.judgeWinLose(GU.toString(), PA.toString()));
        assertEquals(YOU_LOSE, target.judgeWinLose(CHOKI.toString(), GU.toString()));
        assertEquals(YOU_LOSE, target.judgeWinLose(PA.toString(), CHOKI.toString()));
        assertEquals(AIKO, target.judgeWinLose(GU.toString(), GU.toString()));
        assertEquals(AIKO, target.judgeWinLose(CHOKI.toString(), CHOKI.toString()));
        assertEquals(AIKO, target.judgeWinLose(PA.toString(), PA.toString()));
    }
}

==JankenUtilsTestの解説==

  1. \@BeforeClassをつけたinitClass()でテスト対象クラスのインスタンスを生成、フィールド変数に代入します
  2. \@Testをつけたメソッドでテストを実行します
  3. 初めにJankenUtilsには、メソッドの名前だけ作成していたのでテストケースを考えて実装していきます
  4. testCreateJudgeMap()の実装です、テストとしては勝敗判定を行うためのMapを作成できているか?を確認します
    Mapにキーを渡して、想定通りの結果が返ってくるか、テストケースを作成します
  5. テストケースができたら、JankenUtilsクラス本体の実装に入ります
  6. JankenUtils#createJudgeMap()で勝敗判定用のMapを作成、フィールド変数に代入します
  7. JankenUtils#getJudgeMap()で勝敗判定用のMapを取得するようにします
  8. コンストラクタでcreateJudgeMap()を呼び出すようにします
    こうすることで、テストクラスでinitClass()を動かした時点で勝敗判定用のMapが作成されますのであとはgetJudgeMap()でMapを取得するだけです
  9. LOG.info(() -> "*** Helloと入力するテスト ***");はテストで標準入力を受け付けることを示すために出力しているログです
  10. JankenUtils#judgeWinLose()のテストを行うのに引数へユーザーの手と、CPUの手を渡してその勝敗判定結果を返すようにします
  11. テストケースでは、全ケースを網羅する形でテストを行います

このように、テストするメソッドが「どのように動けば良いか?」を確認するための処理(テストケース)を作成して、それから本体の実装に入るようなやり方のことを「テストファースト」と呼び、テスト駆動型開発などとも呼ばれています。

この手法は、実装するメソッドの処理の仕様を明確にする、ということとテストケースを作成するので修正・確認もすぐにできるというメリットがあります。

==ConsoleUtils==

  • printJankenAiko
  • printTe
  • printPonOrSho
  • printJudge

<ConsoleUtilsTest> ※フィールド変数、メソッドのみを記述しています。

/** 標準出力確認 */
private static final ByteArrayOutputStream console = new ByteArrayOutputStream();
/** 改行コード */
private static final String lineSeparator = System.lineSeparator();
/** じゃんけんの時に表示する表 */
private static final String printTable = "****************" + lineSeparator
        + "*グー   = 0    *" + lineSeparator
        + "*チョキ = 1    *" + lineSeparator
        + "*パー   = 2    *" + lineSeparator
        + "****************" + lineSeparator;

/**
 * すべてのテストケースを実行するための準備をする。
 */
@BeforeClass
public static void initClass() {
    // 静的メソッドに修正したのでインスタンス化は不要
//      target = new ConsoleUtils();
    System.setOut(new PrintStream(console));
}

/**
 * このテストクラスの実行終了後に行うべき後始末。
 * 基本的には、フィールド変数のインスタンスなどを開放するが、今回のフィールド変数は
 * 静的フィールド(staticフィールド)なので、アプリ終了時に解放されるので処理なし。
 */
@AfterClass
public static void terminatedClass() {
    // 標準出力を元に戻す
    System.setOut(System.out);
}

/**
 * テストを実行する準備をする
 */
@Before
public void testInit() {
    // 標準出力を空にする
    console.reset();
}

/**
 * 「じゃんけん」を表示する
 */
@Test
public void testPrintJankenAiko_True() {
    ConsoleUtils.printJankenAiko(true);
    assertEquals(printTable + "じゃんけん ..." + lineSeparator, console.toString());
}

/**
 * 「あいこ」を表示する
 */
@Test
public void testPrintJankenAiko_False() {
    ConsoleUtils.printJankenAiko(false);
    assertEquals(printTable + "あいこで ..." + lineSeparator, console.toString());
}

//  /**
//   * 「Sho!」を表示する
//   */
//  @Test
//  public void testPintSho() {
//      ConsoleUtils.printSho();
//      assertEquals("Sho!" + lineSeparator, console.toString());
//  }

/**
 * 「ポン!」か「しょ!」を表示する
 */
public void testPrintPonOrSho_True() {
    ConsoleUtils.printPonOrSho(true);
    assertEquals("ポン!" + lineSeparator, console.toString());
}

/**
 * 「ポン!」か「しょ!」を表示する
 */
public void testPrintPonOrSho_False() {
    ConsoleUtils.printPonOrSho(false);
    assertEquals("しょ!" + lineSeparator, console.toString());
}

/**
 * プレーヤーの勝利の表示
 *
 * @throws Exception 例外時の処理を実装しないのでTHROWS文にしている
 */
@Test
public void testPrintJudge_WIN() throws Exception {
    ConsoleUtils.printJudge(JankenConst.YOU_WIN);
    assertEquals("YOU WIN!" + lineSeparator, console.toString());
}

/**
 * プレーヤーの勝利の表示
 *
 * @throws Exception 例外時の処理を実装しないのでTHROWS文にしている
 */
@Test
public void testPrintJudge_LOSE() throws Exception {
    ConsoleUtils.printJudge(JankenConst.YOU_LOSE);
    assertEquals("YOU LOSE!" + lineSeparator, console.toString());
}

/**
 * 引き分けの表示
 *
 * @throws Exception 例外時の処理を実装しないのでTHROWS文にしている
 */
@Test
public void testPrintJudge() throws Exception {
    ConsoleUtils.printJudge(JankenConst.AIKO);
    assertEquals("DRAW!" + lineSeparator, console.toString());
}

ConsoleUtilsクラスのテストで、ByteArrayOutputStreamというクラスを使用しています。

このクラスは、標準出力(System.out)の出力先をこのクラス(ByteArrayOutputStream)に切り替えるために使用しています。

==ConsoleUtilsTestのコード解説==

  1. \@BeforeClassのついている、initClass()でテスト対象クラスのインスタンス化を行います
  2. これはちょっと特殊な実装ですが、System.setOut(new PrintStream(console));で標準出力の出力先を変更しています
    それは、出力結果を確認するためです、ByteArrayOutputStreamへ出力先を変更し実行結果の確認を行います
  3. \@AfterClassのついたterminatedClass()で変更した標準出力の出力先を元に戻します
  4. \@BeforeのついたtestInit()でconsoleに溜まったデータ(文字列)を空にします
  5. \@Testのついたメソッドでテストを行います、各テストの内容に関しては割愛いたします

<標準出力の出力先を変更する処理>

System.setOut(new PrintStream(console));

上のコードで、標準出力の出力先を切り替えています。

なので、テストクラス「ConsoleUtilsTest」の処理に System.out.println("XXXXX");
とコードを書いても、コンソールにXXXXXは表示されません。

その代わり、assertEquals("ポン!" + lineSeparator, console.toString());のように表示した値を確認する処理を実行しています。

テスト対象クラスの実装ではSystem.out.println()で標準出力に表示しています。下のコードはConsoleUtilsのメソッドです。

public static boolean printJudge(JankenConst resultJudge) /* 追加実装:*/ throws Exception {
    boolean isFinish = true;
    // 勝敗判定結果を表示する
    switch(resultJudge) {
    case YOU_WIN:
        System.out.println("YOU WIN!");
        break;
    case YOU_LOSE:
        System.out.println("YOU LOSE!");
        break;
    case AIKO:
        isFinish = false;
        System.out.println("DRAW!");
        break;
    // 追加実装
    default:
        throw new Exception("想定外の勝敗判定です:" + resultJudge);
    }
    return isFinish;
}

つまるところ、テスト対象クラス「ConsoleUtils」で標準出力に出力している文字列(値)は、テストクラス「ConsoleUtilsTest」にある、console.toString()の処理で取得することができます。

※「追加実装」のコメントは初めに作成していたプログラムから変更した部分です。具体的には、想定外の入力(引数)があった時の例外処理を追加しました。

具体的には下の部分を追加しました。

<追加①>

/* 追加実装:*/ throws Exception {

<追加②>

// 追加実装
default:
    throw new Exception("想定外の勝敗判定です:" + resultJudge);

==ConsoleUtils#printJudge()の解説==

  1. boolean型の変数isFinishをfalseで初期化
  2. resultJudgeの値により、それぞれの表示を行う
  3. 想定外の判定結果があった場合は例外(Exception)を返却する

このように、クラス分け(部品化)すると、テストが楽になったり、実装が楽になったりします。

==筆者の作成したプログラムの実行結果==

testPrintPonOrSho_True()に関しては想定通り右の出力が得られました。「ポン!」※改行コードを含む

testPrintPonOrSho_False()に関しては想定通り右の出力が得られました。「しょ!」※改行コードを含む

他のメソッドに関しても、以下のリンクにあるように、テストの結果が想定通りになっているので、テストは完了した状態です。

自分で実際に作成し、躓くときは、本パートを何度も読み返して見てください。

<JUnitでConsoleUtilsTestを動かして見た時の動画>
IMAGE ALT TEXT HERE

追加修正を行う時のポイント

追加修正を行う場合は、前章でやったとおり「メソッドを追加」することですが前章のやり方だと

「追加処理のためだけのフィールド変数」を作成することができません。それは、クラス分けしていないからです。

クラスごとに「カプセル化」して実装していれば、各クラスの関係を作ったとしても、互いにフィールド変数やメソッドへの影響を与えないように

プログラムの作成を行うことができます。

ここが追加修正のテクニックです。今までの話ありきなので、前章では記述しませんでした。

ここまできたら。。。

ここまでたどり着くのに、かなりの量の学習を行なったと思います。あとはよく考えて、不明点は調べて。。。

なんとか、じゃんけんゲームを作り上げることができると思います。

2. 次の章では

早速、次の「テキストRPG」に関して触れていこうと思います。今までじゃんけんゲームの作成で行ってきた「設計〜テストケースの作成&実装」の要領で、テキストRPGを作成してみようと思います。

じゃんけんゲームと違い、少し処理の量が増えるので「オブジェクト指向プログラミング」の効果がよくわかると思います。

とりあえずは戦闘シーンのみで作成します。

筆者の作成したイメージ(動画)は以下のようなものです。

※「テキストRPG(戦闘シーンのみ)」なのでバージョン0.5になります。
 以下の画像をクリックすると動画が見れます。

IMAGE ALT TEXT HERE