Java はじめて24 〜JUnitでのテスト駆動型開発1〜

イントロダクション

JUnitを使用して開発を行います。と言っても今まで作成していたクラスの追加実装を行うというだけです。ただし、作成する実装が予定通り、仕様通りに動くことを確認しながら作成します。

つまりは、テスト仕様を先に作ってからやります。

まずは、テスト仕様を作成するのですが、これは前回作成したので割愛します。

JUnitを使う

前回、JUnitが動くことを確認するプログラムを作成しました。
なので今回は、設計部分から。。。つまり「どー動けば良いか?」を考えることから実装を始めたいと思います。

参照先一覧

<JUnitでテストケースを作る>

実装内容の確認

ますはソースコードがどのように処理を行っているのか?を確認します。ソースはGithubにアップしてあるのでこれをチェックアウトしていじってみるのもよいです。
ちなみに、余計なプログラムも入っていて、「fx」「opencv」パッケージ以下のファイルは削除しても大丈夫です。

プログラムを追いかける

まずは、どこからプログラムを追いかけたらよいか?これはメインメソッドから追いかけます。
理由としては、以下の通りです。

  • Javaはまずメインメソッドが動く。
  • メインメソッドが複数あっても結局、起動できるのは1つだけ。
  • とりあえずメインメソッドが動く。

そんなわけで、今回参照しているプログラムのある場所を確認します。
パッケージの名前がjp.zenryoku.apps.atmになりますので、ここを開きます。

次に、メインメソッドのある「MainBank」クラスを見ます。

    public static void main(String[] args) {
        MainBank main = new MainBank();
        main.atm();
    }

メインメソッドは、これだけです。このクラス(MainBank)のatm()メソッドを実行しているだけですね。
ちょっと脱線しますが、「MainBankクラスのatm()メソッド」を示すのに次のような書き方をします。「MainBank#atm()

ここから処理を追いかけるのですが、大まかに次のような処理を行っています。

  1. MainBank#atm()を起動する。
  2. コーダー銀行受付文言を表示
  3. 入金するか、引き出しをするか選択してそれぞれの処理を行う。
  4. "bye"と入力するとプログラムが終了する

テスト駆動開発

テストケース(どう動くか?)を作成することから初めていきます。そのためには、まず「何がどう動くのか?」を決めておく必要があります。
その「何がどう動くのか?」を決めているのが「仕様書」になります。

仕様

  • コーダー銀行受付文言を表示を行う。表示内容は「コーダー銀行へようこそ、入金しますか?引き出しますか?」とする。
  • アプリケーションの実行時には、預金額が1000円預金されている。
  • 入金と引き出しができる。
  • "bye"と入力するとアプリケーションを終了する

ファイル操作をする

ファイルを使用してユーザー名とパスワードを管理するようにします。
プログラム的には、指定のファイルが存在すれば、ファイルを参照することができ、ファイルが存在しなけでば、ファイルを作成する。
今回はこの部分のみを実装します。初めにテストクラスの構成ですが、書くテストケースを実行する前にテスト対象クラスのインスタンスを新規で作成します。@Beforeアノテーションをつけてやるとそのメソッドはテストケースの実行前に起動します。

具体的に、テストフォルダ内に同じパッケージを作成して、「テストするクラス名 + Test」という名前のクラスを作成します。
具体的に作成した結果が以下のような形になります。テスト対象クラス「KozaManager」とします。
<PracticeJava1プロジェクト>

PracticeJava1\test\jp\zenryoku\apps\atm\manage\KozaManagerTest.java

テストクラスの作成

ようやく、前置きが終わりました。結論的には、KozaManagerクラスのテストクラスを作成しようということです。
テスト対象のクラスとテストクラスの話をしますので、一応確認します。
※ ここで使用するフレームワークはJUnitです。これを使用して実装するのはとても簡単です。

<補足>
先に、テスト対象クラスのコードを記載していますが、テストコード→テスト対象クラスのコードという順番で作成します

テストコードで以下のように作成します。

  1. テストクラスの作成
  2. テストクラスの初期化「\@Befre」
  3. テストの実行「\@Test」

上記の「\@」アノテーションのついているメソッドがそれぞれに意味を持ちます。

public class KozaManagerTest {
    /** テスト対象クラス */
    private KozaManager target;

    @Before
    public void initClass() {
        target = new KozaManager();
    }
    @Test
    public void testXXX() {
        // 「isFile()」をテストする処理
        assertEquals(true, target.isFile());
    }
}

「isFile()」メソッドは空で作成します。

public boolean isFile() {
    return ????:
}

この時に、「isFile()」の返却値はbooleanでいいのか?値はどのように返すか?など具体的な処理を考えます。

テストケースの次に実装クラス

下記のような順序で処理を実装します。

  1. テストケースを考える。
  2. テスト対象クラスのコードを実装。
  • テスト対象クラス:KozaManager
  • テストクラス:KozaManagerTest

<テスト対象クラスのコード>

/**
 * 口座の管理(登録、更新、削除)を行うクラス
 * @author takunoji
 *
 * 2019/09/21
 */
public class KozaManager {
    /** ファイルへの書き出しクラス */
    private BufferedWriter write;
    /** ファイルの読み込みクラス */
    private BufferedReader read;
    /** 取得(作成)するファイルクラス */
    private File file;
    /** ファイルのパス */
    private static final String FILE_PATH = "resources/koza.csv";

    /** コンストラクタ */
    public KozaManager() {
        // 操作するファイルを指定する
        file = new File(FILE_PATH);
        try {
            write = new BufferedWriter(new FileWriter(file, true));
            if (file.exists()) {
                read = new BufferedReader(new FileReader(file));
            }
        } catch (IOException ie) {
            ie.printStackTrace();
            System.out.println("ファイルオープンに失敗しました。" + ie.getMessage());
            System.exit(-1);
        }
    }

    /** デストラクタ */
    @Override
    protected void finalize() throws Throwable {
        // フィールド変数の後始末
        file = null;
        read.close();
        write = null;
        read = null;
    }

    /** 作成するファイルが存在するかチェック */
    public boolean isFile() {
        return file.exists();
    }

    /**
     * 出力するCSVのヘッダー部分を作成する。
     * @return CSVヘッダーの文字列(カンマ区切り)
     */
    private String createCSVHeader() {
        return "名前, パスワード";
    }

    /**
     * データクラスを受け取り、CSVファイルを出力する(書き出しを行う)
     * @param data コーダー銀行のユーザー情報
     */
    public void dataOutput(Data data) throws IOException {
        if (file.canWrite() == false) {
            throw new IOException("ファイルの書き込みができません: " + file.getAbsolutePath());
        }
        // おおよそのデータサイズを指定すると余計なメモリを使用しなくて済む
        StringBuilder build = new StringBuilder(50);
        // ファイルそ新規で作成するとき
        if (file.exists() == false) {
            // ヘッダー部分の出力
            build.append(this.createCSVHeader());
            // ファイル書き込み処理
            write.write(build.toString());
            write.newLine();
        }
        // StringBuilderのクリア
        build.setLength(0);
        // データ部分の書き込み
        build.append(data.getName() + ",");
        build.append(data.getPassword());
        write.write(build.toString());
        write.newLine();
        write.close();
    }

    /**
     * ファイルを読み込みデータをリストにして返却する
     * @return List<Data> CSVファイルのデータリスト
     */
    public List<Data> readFile() {
        List<Data> list = new ArrayList<>();
        String line = null;
        try {
            while((line = read.readLine()) != null) {
                String[] lineData = line.split(",");
                // lineData[0]: 名前, lineData[1]: パスワード
                list.add(new Data(lineData[0], lineData[1]));
            }
        } catch(IOException ie) {
            ie.printStackTrace();
            System.exit(-1);
        }
        return list;
    }

    /**
     * フィールドのfileを返却する。
     * @return file
     */
    public File getFile() {
        return this.file;
    }
}

大まかな作り方

はじめに、テストクラスの作成をします。するとある程度コードが生成されるので追加でコードを書きます。
まずは、テスト対象クラスをフィールド変数にセットで切るようにします。

public class KozaManagerTest {
    /** テスト対象クラス */
    private KozaManager target;

}

このフィールド変数というのは、クラスの中であればどこらでも参照できるものです。
そして、アクセス修飾子が「private」なので、クラスの中でのみ参照できる変数になります。
クラスの外からフィールド変数にアクセスできないように「private」にするのはセキュリティの基本になります。

そして、テストを実行する時に、テスト対象クラスのインスタンスを作成して各テストケースでテスト対象クラスのインスタンスを参照できるように
下のようなコードを書きます。

@Before
public void initClass() {
    target = new KozaManager();
}
public class KozaManagerTest {
    /** テスト対象クラス */
    private KozaManager target;

    @Before
    public void initClass() {
        target = new KozaManager();
    }
}

今後作成するテストケース=テストを実行するメソッドは次のように書きます。

@Test
public void testXXX() {
    // テストする処理
}

テストケースは、好きなだけ追加できますので、ガンガン追加して大丈夫です。※当然モノによっては重くなるので注意です。

public class KozaManagerTest {
    /** テスト対象クラス */
    private KozaManager target;

    @Before
    public void initClass() {
        target = new KozaManager();
    }
    @Test
    public void testXXX() {
        // テストする処理
    }
}

そして、問題のテストケースですが、プログラムレベルでまずは、ファイルの読み書き用クラスのインスタンスが取得できないと話にならないのでその起動確認を行います。assertを使用できることがJUnitの魅力でもあります。

Assertとは

JUnitのフレームワークで使用できる想定通りに動いたか確認するためのメソッドです。
使い方は以下の通りです。

/**
 * コンストラクタが起動したかどうかを確認する
 * テストケース
 */
@Test
public void testIsFile() {
    assertNotNull(target);
}

上のコードでは、Assertクラスを「static import」して使用しています。のでメソッドを直接呼ぶことができます。JUnitでは、assertNotNullなどの確認用メソッドが用意されています、上の場合だとtargetがNullの時にエラーを吐きます。他にもassertEqualsがありこれは

assertEquals(value, "aaa");

の様に使用し値が等しくない時にエラーを吐きます。

メソッドの中で完結しないのですが、strong>\@Beforeでコンストラクタを起動しているので、テストケースとしては実行済みになりますので、インスタンスがtargetの中に入っているかどうかの確認を行います。

そして、問題の「ファイルを読み込むためのクラス」は以下の様に処理を行っています。下のクラスはテスト対象クラスです。

/** コンストラクタ */
public KozaManager() {
    // 操作するファイルを指定する
    File file = new File("resources/koza.csv");
    try {
        write = new BufferedWriter(new FileWriter(file));
        if (file.exists()) {
            read = new BufferedReader(new FileReader(file));
        }
    } catch (IOException ie) {
        ie.printStackTrace();
        System.out.println("ファイルオープンに失敗しました。" + ie.getMessage());
        System.exit(-1);
    }
}

とりあえずは、このテストがうまくいく様に作成します。
しかし、今回のはコンストラクタを起動するだけ、しかもテストケースと呼べる様なものではないのでよろしくはないのですが、コンストラクタで必要なインスタンスなどを作成しないことには何も始まらないのでこの様になりました。

ポイント

  1. テストケースを作成する前の準備
  2. コンストラクタでテストケースに関連する部分を含むがテストを行ったことにはしない

でわでわ。。。



コメントを残す