似非プログラマのうんちく

「似非プログラマの覚え書き」出張版

JUnit による単体テストの自動化

JUnitJava における単体テストの自動化をサポートするサードパーティー製のライブラリ(フレームワーク)である。

が、これだけの説明では何のことかわからないと思うので、もう少し詳しく説明する。

単体テストの定義と意義

単体テストとは ?

単体テストとは、1 個のクラスだけをテストすることである。テストをする項目が複数のクラスにまたがっていないことが条件である。もちろん、テストするクラスの中で別のクラスを呼び出している場合もあるが、それらのクラスに関しては正しく動作するものと仮定してテストを実施する。

具体例を見てみよう。

package jp.mydns.akanekodou;

/**
 * <p>整数を表す文字列を整数に変換する</p>
 *
 * @version 1.0
 * @author Red cat
 */
public class ChangeNum {
    /**
     * <p>数値文字列を整数に変換する</p>
     *
     * @param str 整数を表す文字列
     * @return 変換された整数値
     * @see java.lang.Integer#parseInt(String)
     */
    public int changeNum(String str) {
        return Integer.parseInt(str);
    }
}
package jp.mydns.akanekodou;

import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;

/**
 * <p>
 * メインクラス<br />
 * 標準入力から取り込んだ文字列を整数値に変換する
 * </p>
 *
 * @author Red cat
 */
public class Main {
    /**
     * <p>メインメソッド</p>
     *
     * @param args コマンドライン引数
     */
    public static void main(String[] args) {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try {
            System.out.print("整数を入力してください: ");
            String str = br.readLine();
            ChangeNum cn = new ChangeNum();
            int value = cn.changeNum(str);
            System.out.println(value);
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}

機能としては単純であるが、単体テストの意味を理解してもらうためにわざと冗長な書き方をしている。

ここで単体テストと言った場合、ChangeNum のみをテストすることである。Main を動かすことなく ChangeNum のみをテストするなんて可能なのか、と思うかも知れないが、それを可能にしてくれるのが JUnit である。

単体テストの意義

なぜ単体テストを実施する必要があるのかについて考えてみよう。実際の開発においては、工程の関係で全部の機能が実装された状態になるまでにはかなりの時間を要することが普通である。その際、未実装の機能に依存することなく個々の機能をあらかじめテストしておかないと、結合テストでバグが発生した場合に手戻り作業が多くなる*1。そこで、個々の機能に関しては動作が保証されていることをまず確認することによって、結合テストでバグが発生した際の原因究明作業を楽にすることが出来る。これが単体テストの存在意義である。

テストを自動化することのメリット

テストを自動化しておくことは、多くのメリットがある。単体テストでバグを発見して、機能を修正したとき、テストを自動化しておけば、再びそのテストを実行するだけで再テストが可能になる。また、バグを修正する場合でも、テストコードが標本となるため、修正作業が容易になる。テストコードはこれすなわち、単体機能の仕様書なのである。

また、テストコードさえ作成しておけば、誰でも同じテストを実行出来るので、テストコードの作成者にテストの負担が偏ることがない。

また、JUnit の作者によれば

なによりも、テスト中に現れる緑色のバーが満タンになるのを見ると気分が良くなる

そうである*2

JUnit による単体テスト

テストコードの作成

では実際に JUnit によるテストコードを作成してみよう。今回は JUnit 4.11 を使用する。

package jp.mydns.akanekodou.test.junit4;

import jp.mydns.akanekodou.ChangeNum;

import org.junit.*;
import static org.junit.Assert.*;
import static org.hamcrest.core.Is.is;

/**
 * <p>JUnit4 による ChangeNum テスト用クラス</p>
 *
 * @author Red cat
 */
public class ChangeNumTest {
    private static ChangeNum target;

    /**
     * <p>
     * テスト前処理<br />
     * 全体を通して 1 回だけ実行
     * </p>
     */
    @BeforeClass
    public static void getTestClass() {
        target = new ChangeNum();
    }

    /**
     * <p>正常系テスト</p>
     */
    @Test
    public void success() {
        String str = "100";
        int expected = 100;
        int actual = target.changeNum(str);
        assertThat("数値に変換されていません。", actual, is(expected));
    }

    /**
     * <p>異常系テスト</p>
     */
    @Test(expected = NumberFormatException.class)
    public void exception() {
        String str = "99.9"; // 整数を表していない !
        target.changeNum(str); // ここで NumberFormatException が発生するはず
    }
}

@Test アノテーションの付いたメソッドがテストメソッドとして認識され、実行される。なお、テストメソッドの実行順は書いた順であることは保証されない。expectedthrow されることが期待される例外クラスを指定することで、例外が発生するかどうかのテストも可能である。

@BeforeClass アノテーションの付いたメソッドは、テスト実行開始直後に、テストメソッドの実行の前に最初の一回だけ実行される。全テストメソッドの終了後に一回だけ実行される @AfterClass もある。これらのメソッドは static でなければならない。

この他、各テストメソッドの実行前に毎回実行される @Before や各テストメソッド実行後に毎回実行される @After もある。

テストの実行

Eclipse においてテストコードを実行するのは簡単である。エディタでソースを開いた状態で右クリックをして「Run As」→「JUnit Test」を選択すればよい。こんな画面が出てくるはずだ。

テストが成功したメソッドは緑色のマークが付いている。失敗した場合は青色のマーク、エラーによってテスト自体が実行できなかったメソッドには赤色のマークが付く。全てのテストメソッドが成功してプログレスバーが緑色で満たされたら、単体テストは成功したことになる。

(参考)JUnit3 による単体テスト

同じ JUnit でも JUnit3 を利用する場合は、以下のようなテストコードになる。

package jp.mydns.akanekodou.test.junit3;

import jp.mydns.akanekodou.ChangeNum;

import junit.framework.*;

/**
 * <p>JUnit3 による ChangeNum テスト用クラス</p>
 *
 * @author Red cat
 */
public class ChangeNumTest extends TestCase {
    private ChangeNum target;

    /**
     * <p>コンストラクタ</p>
     *
     * @param name {@inheritDoc}
     * @see TestCase#TestCase(String)
     */
    public ChangeNumTest(String name) {
        super(name);
    }

    /**
     * <p>
     * テスト前処理<br />
     * 各テストメソッドの前に毎回実行
     * </p>
     *
     * @see TestCase#setUp()
     */
    @Override
    public void setUp() {
        target = new ChangeNum();
    }

    /**
     * <p>正常系テスト</p>
     */
    public void testSuccess() {
        String str = "100";
        int expected = 100;
        int actual = target.changeNum(str);
        assertEquals("数値に変換されていません。", expected, actual);
    }

    /**
     * <p>異常系テスト</p>
     */
    public void testException() {
        String str = "99.9"; // 整数を表していない !
        try {
            target.changeNum(str); // ここで NumberFormatException が発生するはず
            fail("例外が発生していません。");
        } catch(NumberFormatException e) {
        }
    }
}

JUnit4 との主な違いは以下の通り。

  • テストコードのクラスは junit.framework.TestCase を継承する必要がある
  • String 型の引数を一つ持つコンストラクタを定義する必要がある
  • テストメソッドはメソッド名が必ず test で始まっている必要がある
  • @Before に相当するものは setUp メソッドを、@After に相当するものは tearDown メソッドを、それぞれオーバーライドして作る
  • @BeforeClass@AfterClass に相当するものはない
  • 例外発生テストの際、catch の中でエラーメッセージのチェックを行ったり、必要に応じてそれ以降のテストも実施が出来る

*1:個々の機能の中のバグなのか、結合の際に生じているバグなのかをまず突き止めないといけないためだ。

*2:私もそうである。