Java はじめて28 〜JUnitでのテスト駆動型開発5: ファイル出力の実装〜

イントロダクション
前回は実装するべきクラスの実装を行いました。しかし、ファイル入出力の部分は手付かずだったので、その部分を実装しようと思います。

<JUNITの使い方>


今回は、ファイル出力処理を実装します。つまりテストケースの作成と実行を行います。記事の下に動画があるので、参考にどうぞ、

ファイル出力の実装

ファイル出力と言っても、ファイルの作成自体はすでに実装済みなのでデータをファイルに書き込む処理の実装ということになります。
<前回までにできていること>

  1. ファイル作成(koza.csv)
  2. ファイルの存在チェック(KozaManager#isFile)

補足として、ファイルの作成 => new File()ファイルへの書き込み => Writer.write()

ここまでが実装したものになります。ちなみにテストコードは下の様になります。

public class KozaManagerTest {
    /** テスト対象クラス */
    private KozaManager target;
    /**
    *  テストの初期化
    *  各テスト実行前に起動する
    */
    @Before
    public void initClass() {
        target = new KozaManager();
    }

    /**
     * テスト対象クラスのメモリ開放
     */
    @After
    public void terminate() {
        target.finalize();
    }
    /**
    * コンストラクタが起動したかどうかを確認する
    * テストケース
    */
    @Test
    public void testIsInstance() {
        assertNotNull(target);
    }
    /**
    * ファイルの存在チェック処理の確認
    */
    @Test
    public void testIsFile() {
        assertTrue(target.isFile());
    }
    /**
    * ファイルにデータを出力し保存するテストケースです。
    */
    public void testFileCeate() {
        Data data = new Data("名前", "パスワード");
        try {
            target.dataOutput(data);
        } catch(IOException ie) {
            ie.printStackTrace();
            fail();
        }
    }
}

ここでの注意点は、「@Test」のついていないテストケース(メソッド)がファイル出力のテストケースになります。
なので、今回はこのコードを実行しようと思います。
早速「@Test」を上のテストケースにつけます。

/**
* ファイルにデータを出力し保存するテストケースです。
*/
@Test
public void testFileCeate() {
    Data data = new Data("名前", "パスワード");
    try {
        target.dataOutput(data);
    } catch(IOException ie) {
        ie.printStackTrace();
        fail();
    }
}

想定通りにいかない原因

製造中につきものの「動かない。。。」にあたります。
本当であれば、テストコードの中でファイルの中身を確認するのですが、ファイル出力後のテストケースも考えていないので、とりあえず。。。と言ったところです。
さて、想定通りにいかない原因を考えます。というか探します。本体のコードを見直してみると。。。

public void dataOutput(Data data) throws IOException {
    // おおよそのデータサイズを指定すると余計なメモリを使用しなくて済む
    StringBuilder build = new StringBuilder(50);
    // ヘッダー部分の出力
    build.append(this.createCSVHeader());
    // ファイル出力処理
    write.append(build.toString());
    // StringBuilderのクリア
    build.setLength(0);
    // データ部分の出力
    build.append(data.getName() + ",");
    build.append(data.getPassword());
}

「あー、これはファイルの中身がなくて当然だなぁ」と気がつきます(笑)
上のコードでは、出力用文字列作成用のオブエジェクト(StringBuilder)を作成しているだけです。下のようにStringBuilderに文字列を追加しないと出力するべきデータがない状態になります。

build.append(data.getName() + ",");
build.append(data.getPassword());

なので修正します。

public void dataOutput(Data data) throws IOException {
    // おおよそのデータサイズを指定すると余計なメモリを使用しなくて済む
    StringBuilder build = new StringBuilder(50);
    // ヘッダー部分の出力
    build.append(this.createCSVHeader());
    // ファイル書き込み処理
    write.write(build.toString());
    // StringBuilderのクリア
    build.setLength(0);
    // データ部分の書き込み
    build.append(data.getName() + ",");
    build.append(data.getPassword());
    write.write(build.toString());
}

そして、ファイルに出力したらファイルの入出力で使用しているReaderとWriter、そのほかのフィールド変数の後始末をする必要があります。でないとメモリ上にインスタンスが残ってしまいます。Javaの場合はガベージコレクションで最終的にメモリは解放されますが、ここでは明示的に「KozaManger」クラスのインスタンスが解放されるときに各フィールドの解放を行います。(デストラクタに実装)※finalize()を呼び出しましょう。

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

それでは、改めてテストを実行してみます。しかし想定通りに行きませんでした。。。
ファイルは出力しているのですが、肝心の中身が出力されていない状態でした。
原因としては「ファイルを閉じていない」と「ファイルを開くときに追記モードで開いていない」ことでした。
そして、最終的には以下の様なコードになりました。

public void dataOutput(Data data) throws IOException {
    if (file.canWrite() == false) {
        throw new IOException("ファイルの書き込みができません: " + file.getAbsolutePath());
    }
    // おおよそのデータサイズを指定すると余計なメモリを使用しなくて済む
    StringBuilder build = new StringBuilder(50);
    // ヘッダー部分の出力
    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();
}

というわけで、テストをしながら実装するのでやりやすい(色々と試しやすい)実装方法だと思います。
ちなみにテストケースは、色々と試したので少々変わっています。

@Test
public void testFileCeate() {
    Data data = new Data("test", "passwd");
    try {
        target.dataOutput(data);
    } catch(IOException ie) {
        ie.printStackTrace();
        fail("ファイル入出力に問題があります。");
    } catch(Exception e) {
        e.printStackTrace();
        fail("想定外のエラーが起きました。");
    }
}

そして、本体のクラス(メソッド)も同様に修正が入っております。最終的に以下の様な実装になりました。

public void dataOutput(Data data) throws IOException {
    if (file.canWrite() == false) {
        throw new IOException("ファイルの書き込みができません: " + file.getAbsolutePath());
    }
    // おおよそのデータサイズを指定すると余計なメモリを使用しなくて済む
    StringBuilder build = new StringBuilder(50);
    // ヘッダー部分の出力
    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();
}

後、ファイルを開くときに追加書き込みモードで開く必要があるので。。。(今回の実装ではデータの保存を行うのでフィールドで保持する必要があるため)コンストラクタの修正も行いました。

// 修正前のコード
write = new BufferedWriter(new FileWriter(file));
// 修正後のコード
write = new BufferedWriter(new FileWriter(file, true));

実行結果は以下の通りです。

次回は、各処理を(ファイル存在チェック〜ファイル出力)テストします。最終的な確認のコードです。

でわでわ。。。

<<< 前回 次回 >>>

<Java関連の動画リスト>

<JUnit関連の動画リスト>



Java はじめて27 〜JUnitでのテスト駆動型開発4: 追加修正と実装〜

イントロダクション

前回までは、テストケースの作成に注力していましたが。今回は、プログラムを動かすためのクラス、実装の部分に入ろうと思います。
ここでは、イメージ的に、テストと実装を同時にやるようなイメージです。

実装

今回は、前回同様に「JUnitでのテストケースから、テストをクリアできる様に実態クラスを作成します。」が最終的にファイルを出力する前に、出力したファイルの存在チェック処理を作る必要があるので、そちらの処理(テストケース)を先に作成します。

追加修正

前回までに作成したものをまとめると以下の様になります。
<口座管理処理の要件>

  1. ファイルを操作するためのオブジェクトを作成する
  2. ファイルにデータをCSV形式で書き出す。
  3. ファイルが存在するのであれば、それを読み込みデータを保持する

<実装済み>
1のオブジェクト作成のみ

<テストケース>
ファイル出力のテストケースとKozaManagerクラス(本体)の実装時に、以下の不足する実装(処理)があることに気がつく

  1. 出力するCSVのヘッダー部分の作成処理
  2. デストラクタ
  3. 作成したファイルの存在確認
  4. ファイルの内容確認

上記の様なことが抜けていたので、作成するテストケースも作成順序が変わってきてしまいました。元の設計に抜けがあった様です。つまり、以下の様な順序で作成することになります。

  1. ファイルの存在チェック処理
  2. ファイルにCSVデータを出力する処理
  3. ファイルの内容確認処理
  4. ファイル出力の処理(上の1-3を全て通して行う確認)

余談ですが、テストケースを作成することで「不足ケース」を見つけることができます。当然不足があれば、テストは想定通りには動きません、プログラムですから「気を利かして。。。」なんてことはやってくれません(笑)

ファイル存在チェック

はっきり言って1行で終わります。
コンストラクタで参照(作成)するファイルオブジェクトは作成しているのでそれを使用します。
なので、もともと下の様なコンストラクタを使っていましたが、修正します。
<元のコード>

public class KozaManager {
    /** ファイルへの書き出しクラス */
    private BufferedWriter write;
    /** ファイルの読み込みクラス */
    private BufferedReader read;

    /** コンストラクタ */
    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);
        }
    }

    /** デストラクタ */
    @Override
    protected void finalize() throws Throwable {
        write = null;
        read = null;
    }

    /**
     * データクラスを受け取り、CSVファイルを出力する(書き出しを行う)
     * @param data コーダー銀行のユーザー情報
     */
    public void dataOutput(Data data) {
    }
}

<修正後のコード>
・フィールドを追加します。
・コンストラクタの「file」をフィールド変数のFIleオブジェクトに変更します。

public KozaManager() {
    // 操作するファイルを指定する
    file = new File(FILE_PATH);
    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);
    }
}

本来であれば、ファイルの作成はプログラムからやるのですが、順番的にファイルの存在チェックを先にやるので手で作成します。
以下の様なファイルです。

名前, パスワード
テスト太郎, 1234
テスト花子, 2345
テストたくのじ, 3456

これを「koza.csv」と名前をつけてEclipseのresourcesフォルダの直下に配置(作成)します。。。
が、以前起動したテスト(コンストラクタのテスト)の実行時にファイルが作成されていたので(ファイルの中身は空)、テストを起動した時点でファイルが存在することになります。
なので、テストケースとしては以下の様なものになります。

public void testIsFile() {
    assertTrue(target.isFile());
}

これも1行で終わりました(笑)

しかし、これでテストを実行しても上のコードはテストとして実行されません、なぜなら「@Test」がついていないからです。
なので以下の様にテストクラスを修正します。

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

    /**
     *  テストの初期化 
     *  各テスト実行前に起動する
     */
    @Before
    public void initClass() {
        target = new KozaManager();
    }

    /**
     *  テストの後始末 
     *  各テスト実行前に起動する
     */
    @After
    public void terminate() {
        target.finalize();
    }

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

    /**
     * ファイルの存在チェック処理の確認
     */
    @Test
    public void testIsFile() {
        assertTrue(target.isFile());
    }

    /**
     * ファイルにデータを出力し保存するテストケースです。
     */
    public void testFileCeate() {
        Data data = new Data("名前", "パスワード");
        try {
            target.dataOutput(data);
        } catch(IOException ie) {
            ie.printStackTrace();
            fail();
        }
    }
}

ここでのポイントはファイル出力のテストケースに「@Test」がついていないところです。つまりこのテストケースは実行されません。では動かしてみます。

とりあえずはテスト成功の様です。ここでファイルの存在チェックができたので、次はファイル出力のテスト実装に入ろうと思います、

でわでわ。。。

<<< 前回 次回 >>>

<Java関連の動画リスト>

<JUnit関連の動画リスト>



Java はじめて26 〜JUnitでのテスト駆動型開発3: クラスの実装〜

イントロダクション

JUnitを使用したテスト駆動開発について記載しています。
前回は、どのようにテストケースを実装したら良いか?とか実装する手法について記載しました。

<JUnitの起動設定1>

<同様に2>

Junitのサンプルコード

JUnitでのテストを行うために必要なことは

  1. junit.jarを設定
  2. テストメソッドに「\@Test」をつける

より簡単にテストを行いたければ、「/@Before」などのアノテーションを使う。

public class TestSample {
    /** テスト対象クラス */
    private Sample target;
    @Before
    public void initTest() {
        // テストの実行準備の処理
        target = new Sample();
    }
    @Test
    public void test001() {
        assertEquals("a", "a"); // 二つの引数が等しいことを確認できる。
    }
}

クラスの実装

今回は、JUnitでのテストケースから、テストをオールグリーンに出来る様に実体クラスを作成します。
つまり、JUnitでテストケースを要件を満たす様に作成し、テスト実行後に緑色の表示がされる様にKozaManagerのメソッドを作成します。これは、銀行のATMを簡易的にしたコンソールアプリを作成するための実装でもあります。
ちなみに、今回の実装する時の要件は以下の様になっています。

  1. ファイルを操作するためのオブジェクトを作成する
  2. ファイルにデータをCSV形式で書き出す。
  3. ファイルが存在するのであれば、それを読み込みデータを保持する

クラスを仕分けしてからの実装を行うので、どの様に考えれば良いか?の指針になれば良いと思います。

<クラスの仕分けの例>

  • File操作担当のクラスを作成する(仮にFileUtilクラスとする)
  • KozaManagerからFile操作をするときはこのクラスを呼ぶようにする

<KozaMangerでの実装例>

public class KozaManger {
    ...
    public boolean isFile() {
        return FileUtil.isFile("reources/koza.csv");
    }
    ...
}

<テストケース(メソッド)の例>

@Test
public void testIsFile() {
    // 存在しないファイルを指定したときはFALSEを返す
    boolean isFalse = FileUtil.isFile("resources/koza.xyz");
    assertFalse(isFalse);
    // 存在するファイルを指定したときはTRUEを返す
    boolean isTrue = FileUtil.isFile("resources/koza.csv");
    assertTrue(isTrue);
}

<FileUtilでの実装例>※テストしていません。。。
※JavaDocを参照:File#isFile()

public static boolean isFile(String path) {
    File file = new File(path);
    return file.isFile(path);
}

<ファイル入出力のテスト>

今までの概要

前回作成したのは、上の要件のうちの1(コンストラクタで実装)だけです、これはテストを実行する準備段階でもあるので、テストケースとしては「インスタンスがtargetに設定されていること」を確認するテストケースを作成しました。

public class KozaManager {
    /** ファイルへの書き出しクラス */
    private BufferedWriter write;
    /** ファイルの読み込みクラス */
    private BufferedReader read;
    /** コンストラクタ */
    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);
        }
    }
    /** デストラクタ */
    @Override
    protected void finalize() throws Throwable {
        write = null;
        read = null;
        }
    /**
    * データクラスを受け取り、CSVファイルを出力する(書き出しを行う)
    * @param data コーダー銀行のユーザー情報
    */
    public void dataOutput(Data data) {
    // スタブの状態(空実装)
    }
}

コンストラクタと、デストラクタを実装しています。
コンストラクタは、ファイル操作に必要なクラスのインスタンスを作成します。
デストラクタは作成したインスタンスを解放(Nullをセット)します。※メモリの開放をすることでリソースの節約になります。

作成したデストラクタは明示的に呼び出しましょうつまり使い終わったら下のように「finalize()」を呼び出しましょう。
ガベージコレクションに任せると、いつ呼び出されるかわかりません。。。

KozaManager kozaManager = new KozaManager();
// 何かしらの処理 ...

// メモリの開放 finalize()を明示的に呼び出す
kozaManager.finalize();

現状のテストコード

package jp.zenryoku.apps.atm.manage;
import static org.junit.Assert.assertNotNull;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import jp.zenryoku.apps.atm.data.Data;
/**
* @author takunoji
*
* 2019/09/25
*/
public class KozaManagerTest {
    /** テスト対象クラス */
    private KozaManager target;
    /**
    *  テストの初期化
    *  各テスト実行前に起動する
    */
    @Before
    public void init() {
        target = new KozaManager();
    }

    /**
     * テスト終了時にメモリの開放処理を行う
     */
     @After
     public void finish() {
        // デストラクタを明示的に呼び出す。
        target.finalize();
     }
    /**
    * コンストラクタが起動したかどうかを確認する
    * テストケース
    */
    @Test
    public void testIsFile() {
        assertNotNull(target);
    }
    /**
    * ファイルにデータを出力し保存するテストケースです。
    */
    public void testFileCeate() {
    }
}

テストケース作成

今回の目玉になります、現状はから実装になっている「testFileCreate()」の実装を行いますが、これはテスト対象クラス(本来作成するクラス)がどう動けば良いか?を考えて作成する必要があります。

どう動けば良いか?

口座情報管理(KozaManager)クラスで入力されたデータをファイルに出力する必要があるので単純に「データを受けて、それをCSVに変換してファイル出力する」処理を行うのがこのメソッドの処理になります。
まとめると以下の様になります。

  1. 入力としてDataクラス(口座情報クラス)を受ける
  2. 出力として、CSVファイルを出力する

ここで、KozaManagerクラスの他に「Dataクラス」が必要であることがわかります。これは前回作成しております
そして、CSVファイルを出力するのに、ヘッダー部分のテンプレート(CSVファイルのヘッダは固定値です)を出力する必要がありますので、これをKozaManagerに追加します。
現状では、以下の様なコードになります。

/**
* 出力するCSVのヘッダー部分を作成する。
* @return CSVヘッダーの文字列(カンマ区切り)
*/
private String createCSVHeader() {
    return "名前, パスワード";
}
/**
* データクラスを受け取り、CSVファイルを出力する(書き出しを行う)
* @param data コーダー銀行のユーザー情報
*/
public void dataOutput(Data data) throws IOException {
    StringBuilder build = new StringBuilder();
    build.append(this.createCSVHeader());
    build.append(data.getName() + ",");
    build.append(data.getPassword());
}

しかし、ここで注意したいのがCSVデータは大雑把に以下の様な形式になります。そして、サンプルデータを記載しておきます。
<形式>
1行目: ヘッダー
2行目以降: データ

名前, パスワード
テスト太郎, 1234
テスト花子, 2345
テストたくのじ, 3456

頭をひねる

ここで、元のクラスに色々と修正を加える必要が出てきました。追加する修正は以下のものです。

  1. ファイル出力する時のパス指定をハードコーディング(File file = new File("resources/koza.csv");)しているのを定数としてクラスに保持したい
  2. ファイルが出力できたか?のファイル存在チェックメソッドが欲しい

これらの修正を加えるのに、テストケースで「ファイルにデータをCSV形式で書き出す。」という処理は最後になります。
上の処理を確認するのにファイルの存在チェックなどが必要なので先にそちらの処理を作成する必要があるからです。

先にファイル存在チェック処理を作る

これをやる必要が出てきました。
これは次回やろうと思います。

Java はじめて27 〜JUnitでのテスト駆動型開発4: 追加修正と実装〜

でわでわ。。。

<<< 前回 次回 >>>

<Java関連の動画リスト>

<JUnit関連の動画リスト>



Java はじめて25 〜JUnitでのテスト駆動型開発2: テストケース作成〜

イントロダクション

前回のまとめ

JUnitがなんなのか?に関して記載しました。
そして、このフレームワークはテスト(作成したコード)を実行してその動きが想定通りに動いたかどうか?を「assert」を使用して確認できるものです。

動かすためのポイント
junit.jarがビルドパスに設定されている

テストケース作成

今回は、コンストラクタがエラーもなく正常に動いたかどうかの確認を行うところから始めます。

大まかに以下の様な形で実装します。

public class KozaManagerTest {
    /** テスト対象クラス */
    private KozaManager target;
    /**
    *  テストの初期化
    *  各テスト実行前に起動する
    */
    @Before
    public void initClass() {
        target = new KozaManager();
    }
    /**
    * コンストラクタが起動したかどうかを確認する
    * テストケース
    */
    @Test
    public void testIsFile() {
        assertNotNull(target);
    }
}

今回のテストケースを実装していますが、ポイントとしては、以下の点が挙げられます。

  1. @Beforeは各テストケース(メソッド)が起動する前に動く
  2. @Testはテストケース(メソッド)に付ける

上のコードでは、コンストラクタが起動してフィールド変数「target」にインスタンスが設定されているかどうか?(正確には、Nullではない事)を確認します。targetにインスタンスが設定されていない=コンストラクタが起動している最中に例外(Exception)が発生して@Befireのついているメソッドの処理target = new KozaManager()の処理が終わらないうちに(targetに値が設定されないうちに)例外を受け取る処理が動くため、値が設定されない=Nullになるわけです。

<余談>
\@Beforeアノテーションはテストケースが開始される前(Before)に実行覚ます。もし、インスタンスの生成は一回だけでよいのであれば「\@BefreCLass」を使いましょう。JUnit5では\@Before -> \@BeforeEach \@BeforeClass -> \@BeforeAllに代わっているようです。

具体的に

下の動画を見てもらうとわかると思います。

この処理は、コンストラクタで例外を発生させています。

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));
            throw new IOException("テスト例外");
        }
    } catch (IOException ie) {
        ie.printStackTrace();
        System.out.println("ファイルオープンに失敗しました。" + ie.getMessage());
        System.exit(-1);
    }
}

ここでのポイントは例外が発生した後にアプリ(メインメソッドの処理)を強制終了しているのでJUnitの緑色とか茶色(エラー時)が表示されません。この場合はテストも結果が出ません。

<補足>
ちなみに、コードを実行した後にガベージコレクションでフィールド変数などが解放されるので実装しなくても大丈夫なのですが、テストケースでなんどもKozaMangerクラスのインスタンスを生成するのでC言語的にいうところの「デストラクタ」(コンストラクタの逆=インスタンスを削除する時)が動く時にフィールド変数を解放します。この処理を追加します。ちなみに、「finalize()」はObjectクラスに定義されているメソッドです。そのため「オーバーライド」しています。

「finalize()は書いてはいけない!」みたいな記事がありましたが、「ガベージコレクションに任せるな!」という意味です。
メモリの開放などは、finalize()を明示的に呼び出しましょう

もしくは、インスタンスの開放を明示的にやりましょう

/** デストラクタ */
@Override
protected void finalize() throws Throwable {
    write = null;
    read = null;
}
public static void main(String[] args) throws Throwable {

    try {
       Sample.execute();
    } catch (Exception e) {
       e.printStackTrace();
    } finally {
      // 明示的に呼び出す。
       Sample.finalize();
    }
}

こうすることで、メモリに余計なもの(オブジェクトやインスタンス)を保存しないので、処理が快適に動きます。
「ガベージコレクションがあるからいいや」と思っていると、いつメモリの開放をするかわからないので明示的にメモリの開放処理をしましょう。
ということです。

テストケースの実装

ここからが本当のテスト駆動型開発です、現状はコンストラクタを作成していますが、説明のため。。。言い訳くさい。。。

とりあえず、以下の様に考えます。

「このクラスで要件を満たすために何をしたら良い?」

自分の答えは以下の通りです。

  1. ファイルを操作するためのインスタンスを作成する
  2. ファイルにデータをCSV形式で書き出す。
  3. ファイルが存在するのであれば、それを読み込みデータを保持する

項目1はコンストラクタで実現しているので。今回は2のファイルにCSVデータを書き出す。を実装しようと思います。
設計としては以下の様にします。
<設計>

  1. ファイルの書き出しを行うメソッドはdataOutputという名前にする
  2. 書き出しを行うときはCSV形式で出力する
  3. データを受け取るときは「Dataクラス」で受け取る

この設計から行くと作成するメソッドは「dataOutput」で引数は「Dataクラス」になります。
そして、返却値は「なし=void」です。

なので、テストケースとしては以下の様なコードになります。

/**
  * ファイルにデータを出力し保存するテストケースです。
 */
  public void testFileCeate() {
    Data data = new Data("名前", "パスワード");
    target.dataOutput(data);
}

この様に実装しましたが、「Data」クラスも「dataOutput」メソッドも存在しないのでエラーになります。
なのでこれを「スタブ=空実装(名前だけ、形だけ)」で作成します。そして、作成するDataクラスは以下の様な形で実装します。
これはデータクラスで、内容はフィールドと「Getter(ゲッター)」と「Setter(セッター)」のみになります。

public class Data {
    /** ユーザー名 */
    private String name;
    /** パスワード */
    private String password;
    /**
      * @return the name
      */
      public String getName() {
        return name;
        }
    /**
      * @param name the name to set
      */
      public void setName(String name) {
        this.name = name;
        }
        /**
          * @return the password
          */
        public String getPassword() {
            return password;
        }
        /**
          * @param password the password to set
          */
        public void setPassword(String password) {
            this.password = password;
        }
}

そして、今回作成するメソッドをスタブで作成します。スタブで作成すれば、処理の中身がないけれど、呼び出し元ではエラーなく呼ぶことができます。なのでIN(引数)とOUT(返り値)だけ定義してあれば良いのです。
さらに、データクラスをインポートします。
import jp.zenryoku.apps.atm.data.Data;

<テストケースの実装と作成するクラス>

<スタブメソッド>※返り値なしの場合です。

/**
* データクラスを受け取り、CSVファイルを出力する(書き出しを行う)
* @param data コーダー銀行のユーザー情報
*/
public void dataOutput(Data data) {
}

次回は、このスタブで要件を満たすことができるかどうか考え、必要な実装を行いたいと思います。

Java はじめて26 〜JUnitでのテスト駆動型開発3: クラスの実装〜

ここの部分が実装になります。

実装したらテストして、修正…を繰り返す形で実装します。テスト駆動だと作った処理が想定通りか確認する事とテストが同じなので一手間省けます。

でわでわ。。。

<<< 前回 次回 >>>

<Java関連の動画リスト>

<JUnit関連の動画リスト>



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. コンストラクタでテストケースに関連する部分を含むがテストを行ったことにはしない

でわでわ。。。