初出 JAVA PRESS Vol.34 (2004.01)

JUnit+サーバサイドテストの忘れちゃいけない必須テクニック

JUnit 編 テストの真髄を掴む

本当に正しいテストを行っていますか ?

みなさんは前号(Vol.33)の「車窓からのTDD」(1) を読まれましたか。TDD (Test-Driven Development) (2) もしくはテストファーストについてとても分かりやすく解説が行われていました。そこで重要な役割として登場するのが、JUnitです。

前号を読まれていない方もいらっしゃると思うので、簡単に説明すると、TDDとはテストをベースにしたソフトウェアの開発手法です。通常の手法とは異なり、まずテストから先に記述してから実装を行います。

JUnitはTDDで自動テストを行うために使用するテスティングフレームワークです。決められた手順でテストを記述すれば、JUnitが自動的にテストを実行することができます。

本解説を読んでいただいて、もう一度「車窓からのTDD」を読んでいただければ、よりTDDに関する理解が深まると思います。もちろん、本解説だけでもTDDの一端には触れられるようにしたいと思っています。

JUnit のインストール

Junit をインストールするにはまずhttp://www.junit.org/からjunit3.8.1.zipをダウンロードします。原稿執筆時点での最新バージョンは 3.8.1 です。

ZIPファイルを解凍すると、junit3.8.1というディレクトリが作成されます。図 1にZIP ファイルを解凍してできるファイル構成を示しました。

JUnit の本体は junit.jarというJARファイルです。その他にJUnitのソース、ドキュメント、JavaDoc、サンプルなどが含まれています。

JUnitのインストールは使用時にjunit.jarをクラスパスに加えるだけです。

JUnitのように常に使用するツールは<j2sdkをインストールしたディレクトリ>\jre\lib\extディレクトリ(Unixでは\を/で置きかえてください)におきたいのですが、JUnit のドキュメントにはこのディレクトリに配置するとテストのクラスを見つけられないことがあると記載されています。

実際にはextディレクトリに配置してもほとんど問題がありませんが、テストが見つけられないという例外が発生したらまずこの点を疑ってみてください。

 

junit3.8.1.zip の構成
図 1 junit3.8.1.zip の構成

JUnitの使い方

JUnitは3種類の使用方法があります。テキストベースとGUIベースのもので、GUIベースにはAWT版とSwing版があります。JUnitのメインクラスはすべてTestRunnerクラスですが、パッケージが異なります。

junit.textui.TestRunner テキスト版
junit.awtui.TestRunner AWT版
junit.swingui.TestRunner Swing版

Swing版はテストをツリー状に見ることができるなどAWT版に比べて機能が追加されているので、できればSwing版をお使いになることをお勧めします。

インストールが正しく行われているか確かめるためにサンプルを動かしてみましょう。junit3.8.1ディレクトリで次のようにTestRunnerを起動させてみてください。

ここではWindowsXPで起動させた場合を示します。

c:\junit3.8.1> java -cp junit.jar;. junit.swingui.TestRunner junit.samples.AllTests

TestRunnerを引数つきで起動させると自動的にテストを開始します。起動すると、図2のようなフレームが表示されます(丸数字は筆者が説明のために付記したものです)。モノクロームでは分かりにくいのですが、図2-5 のバーが緑になっていればテストが成功していることを示しています。

TestRunnerのインタフェースをもう少し詳しく見ていきましょう。図2の丸数字は次のようなことを示しています。

  1. テストのクラス。ここに表記されたテストを行います。直接記入することもできます。
  2. テストを選択するためのダイアログを表示するボタン。選択ダイアログでは、クラスパス中に存在するテストクラスが一覧でき、選択することができます。
  3. テストの開始。テストは何度も行うことが可能です。
  4. テストごとにクラスのローディングを行うかどうかを示すチェックボックス。ここをチェックしておけば、テストのたびにクラスのリロードを行うことができます。
  5. テストの結果を示すバー。テストが成功すれば緑、失敗したときには赤になります。TDDの「赤、緑、リファクタリング」というのはこのバーの色を示しています。
  6. テストの総数における成功したテストの数。
  7. 例外によりテストが失敗した数。
  8. テストが失敗した数。
  9. 失敗した例外の一覧。
  10. すべてのテストのツリー表示。成功したテストは緑の、失敗すると赤またはグレーの×で表されます。
  11. テストが失敗したときのメッセージを表示する。

7. と 8. の違いは、8. が記述したテストが失敗した場合を示し、7. が予期しない例外が発生してテストが失敗した場合を示しています。

TestRunnerの使い方に難しいところはありませんので、すぐに使いこなすことができると思います。

 

TestRunner
図 2 Swing 版 TestRunner

 

テストの書き方

テストはjunit.framework.TestCaseの派生クラスとして記述します。

テストのクラスはなるべく判別がつきやすいようにTestXXXXXとするか、XXXXXTestというようなクラス名にすることをお勧めします。XXXXXの部分にはテストしたいクラス名にします。

Testを前にするとエクスプローラなどでテストクラスを容易に判別することができます。また、Testを後ろにするとテストされるクラスと並んで示されるので、テストとテストされるクラスの対応が分かりやすくなるという利点があります。

最終的にはTestが前にくるか後にくるかは個人の好みなので、どちらでもお好きな方でどうぞ。

テストの実体はtestで始まるパブリックで引数なし、戻り値がvoidのメソッドに記述します。たとえばequalsメソッドをテストするなら、testEqualsなどの名前にします。

メソッドの中で表1に示したAssertクラスのメソッドを使用してテストを記述します。TestCaseクラスはAssertクラスの派生クラスなのでこれらのメソッドをそのまま使用することができます。

よく使用されるのがassertEqualsやassertTrueです。前者は2つの引数を比較し、同値であればテストが成功します。同様のメソッドにassertSameがありますが、assertEqualsメソッドはequalsメソッドを使用して比較を行うのに対し、assertSameはオブジェクトが同じものかどうかを調べます。

assertEqualsメソッドなど2つの引数を取るものは、引数の並び順の前の方が期待される値で、実際の値は後ろに書きます。

assertTrueメソッドは条件式が真であるかどうかをテストします。したがって次の2つの式は同じことを意味しています。

    assertEquals(expected, actual);
    assertTrue(expected.equals(actual));

これらのメソッドはすべて第1引数に文字列を取るものがオーバロードされています。これらのメソッドを使用するとテストが失敗したときに引数で指定した文字列が表示されます。

JUnitはtestで始まるメソッドをリフレクションを利用して検出し、自動的にテストを行います。

テストケースでのメソッドの並びとテストの順序は対応しません。たとえばtestAメソッドとtestBメソッドがこの順で記述されていたとしても、testAが先に行われる保証はありません。このため、それぞれのテストメソッドは独立にして、テストメソッド間で依存がないようにする必要があります。

それぞれのテストで同一の初期化や終了処理がある場合は、setUpメソッド、tearDownメソッドに記述することができます。

文章だけだとよく分からないので、次章で具体的にTDDに則ってサンプルを作ってみましょう。

表 1 JUnitのアサーションメソッド
メソッド名 機能
assertEquals 2つの値が同値であることをテストする。
assertSame 2つのオブジェクトが同一であることをテストする
assertNotSame 2つのオブジェクトが同一でないことをテストする
assertNull オブジェクトがnullであることをテストする
assertNotNull オブジェクトがnullでないことをテストする
assertTrue 指定された条件がtrueであるかどうかをテストする
assertFalse 指定された条件がfalseであるかどうかをテストする
fail テストを失敗させる

 

TDDの基本的な進め方

「車窓からのTDD」でも触れられていましたが、JUnitを使用したソフトウェアの開発の基本は

赤 - 緑 - リファクタリング

です。通常の流れは次のようになります。

  1. テストメソッドを記述する
  2. コンパイルが失敗することを確かめる
  3. コンパイルが通るような最小限の実装を行う
  4. テストが失敗することを確認する
  5. テストが通るような実装を行う。テストに通すためには次の3つの手法が使われる
    • フェイク
    • 明らかな実装
    • トライアンギュレーション
  6. テストが成功することを確認する
  7. 4で行った実装をリファクタリングする
  8. 1に戻る

この流れにそって簡単なサンプルを作ってみましょう。作るのは電話帳です。機能としては電話番号の登録と検索だけという簡単なものです。名前はPhoneBookクラスとしましょう。サンプルなので、とりあえず日本語は考えないことにしておきます。

繰りかえしになりますが、TDDではテストから書き始めます。PhoneBookクラスをテストするためにTestPhoneBookクラスを作成します。

まず、電話帳に番号を登録するテストから書いてみましょう。PhoneBookオブジェクトを生成して電話番号を登録します。生成したときには登録されている電話番号はないはずですが、登録すれば登録数が増加するはずです。そこで、リスト1のようなテストコードを書いてみました。

import junit.framework.TestCase;
 
public class TestPhoneBook extends TestCase {
    public void testRegister() {
        PhoneBook book = new PhoneBook();
        assertEquals(0, book.size());
 
        book.register("Yuichi Sakuraba", "00-1234-5678");
        assertEquals(1, book.size());
    }
}
リスト 1 TestPhoneBookクラスの第1バージョン

TestPhoneBookクラスはjunit.framework.TestCaseの派生クラスとして定義します。そして、テストを行うtestRegisterメソッドを記述します。その中でPhoneBookオブジェクトを生成し、sizeメソッドを使用して登録数を取得します。

そして、assertEqualsメソッドを使用して登録数が0であることをテストします。前述したようにassertEqualsメソッドは期待値を第1引数に書き、テストする値を第2引数に指定します。

次に電話番号を登録して、もう一度sizeメソッドで登録数が増加していることをチェックします。

このテストコードをコンパイルするとコンパイルは当然のことながら通りません。そこで、コンパイルを通すために必要最小限の実装を行います。これをリスト2に示しました。

public class PhoneBook {
    public void register(String name, String number) {}
 
    public int size() {
        return 0;
    }
}
リスト 2 PhoneBookクラスの第1バージョン

さっそくJUnitでテストです。バーが赤くなったのが確認できたでしょうか。確認できたら今度はバーを緑にしましょう。リスト3がそのコードです。リストの下線を引いた部分が変更したところです。これからはコードの追加・変更があった部分を下線で示すようにしました。

public class PhoneBook {
    private int size;
 
    public void register(String name, String number) {
        size++;
    }
 
    public int size() {
        return size;
    }
}
リスト 3 PhoneBookクラスの第2バージョン

「そんなコードでいいの」と思われる方も多いと思いますが、緑にすることが最優先なので、実装は適当でも構いません。この適当にでっち上げたコードを書くことがフェイクです。

こんなコードでもJUnitのバーが緑になりました。不安かもしれませんが、今はこのまま放っておいて次に行きましょう。

次は電話番号登録のテストを追加しましょう。

同じ人が複数の電話番号を登録するのは構いませんが、同じ電話番号を違う人が登録することを禁止することにしましょう。これをテストするためのコードを追加します(リスト4)。下線で示したように、registerメソッドの戻り値で登録が行えたかどうかを判定できるようにしました。さらに重複に関するテストを追加しています。

import junit.framework.TestCase;
 
public class TestPhoneBook extends TestCase {
    public void testRegister() {
        PhoneBook book = new PhoneBook();
        assertEquals(0, book.size());
 
        assertTrue(book.register("Yuichi Sakuraba", "00-1234-5678"));
        assertEquals(1, book.size());
 
        // 同じ名前で複数の番号は登録できる                           
        assertTrue(book.register("Yuichi Sakuraba", "00-0123-4567")); 
        assertEquals(2, book.size());                               
                                                                    
        // 同じ番号は複数登録できない                                 
        assertFalse(book.register("James Gosling", "00-1234-5678")); 
        assertEquals(2, book.size());    
    }
}
リスト 4 TestPhoneBookクラスの第3バージョン

このままだと当然コンパイルはできません。コンパイルができないことを確認したら、ソースを変更します。とりあえずPhoneBook#registerメソッドの戻り値をbooleanに変更しましょう。

TestRunnerを実行してみると当然ながらバーが赤になります。

追加したテストを通すためにはどうすればいいでしょうか。登録した電話番号を保持しておくのがよさそうです。そして、新たに登録するときに今まで登録された番号と照合を行えばいけそうです。

さっそくこれをやってみたのがリスト5です。電話番号を保持させるのにはマップを使用しました。ただし、電話番号は重複が許されないので、電話番号をキーとして値を名前にしました。

import java.util.HashMap;
import java.util.Map;
 
public class PhoneBook {
    private int size;
    private Map records;
 
    public PhoneBook() {
        records = new HashMap();
    }
 
    public boolean register(String name, String number) {
        if (records.containsKey(number)) { 
            return false; 
        } else { 
            records.put(number, name); 
            size++;
            return true; 
        }
    }
 
    public int size() {
        return size;
    }
}
リスト 5 PhoneBookの第3バージョン

電話番号の照合には containsKey メソッドを使用しています。登録されてなければ size をインクリメントします。

これで緑になりました。

このように複数のテストを用いて、テストを成功するような実装を行うことをトライアンギュレーションといいます。フェイクと違ってトライアンギュレーションで実装したコードは質が高くなります。

さてここで、リファクタリングをしましょう。バーを緑にするために導入したnumberですが、Mapを使用することで役目が終わったようです。これを取り去ってしまいましょう(リスト6)。下線で示したようにMap#sizeメソッドを使用して登録数を返すようにしています。

import java.util.HashMap;
import java.util.Map;
 
public class PhoneBook {
    private Map records;
 
    public PhoneBook() {
        records = new HashMap();
    }
 
    public boolean register(String name, String number) {
        if (records.containsKey(number)) {
            return false;
        } else {
            records.put(number, name);
            return true;
        }
    }
 
    public int size() {
        return records.size();
    }
}
リスト 6 リファクタリングしたPhoneBookの第3バージョン

リファクタリングを行ったらJUnitのバーが緑のままであることを確認しましょう。これはリファクタリングを行ったことでクラスのふるまいが変化していないかをチェックをするためです。このようなテストを回帰テストといいます。

次は電話番号の検索です。検索のテストをtestLookupメソッドに記述しました(リスト7)。

import junit.framework.TestCase;
 
public class TestPhoneBook extends TestCase {
    private PhoneBook book;
 
    public void setUp() { 
        book = new PhoneBook(); 
    }
 
    public void testRegister() {
        assertEquals(0, book.size());
 
        assertTrue(book.register("Yuichi Sakuraba", "00-1234-5678"));
        assertEquals(1, book.size());
 
        // 同じ名前で複数の番号は登録できる
        assertTrue(book.register("Yuichi Sakuraba", "00-0123-4567"));
        assertEquals(2, book.size());
 
        // 同じ番号は複数登録できない
        assertFalse(book.register("James Gosling", "00-1234-5678"));
        assertEquals(2, book.size());
    }
 
    public void testLookup() { 
        List results = book.lookup("sakuraba"); 
        assertEquals(0, results.size());
    }
}
リスト 7 testLookupメソッドを加えたTestPhoneBookクラス

testRegisterメソッドとtestLookupメソッドはPhoneBookオブジェクトを生成する部分は同一なので、これをsetUpメソッドにまとめてしまいましょう。

setUpメソッドはこのように初期化のためのコードをまとめるために使用されます。同様にtearDownメソッドは終了処理をまとめます。

まずはなにも登録されていない状態で検索してみましょう。もちろん、該当するものはありません。

検索結果は複数あるかもしれないので、lookupメソッドの戻り値はリストにしてみました。とりあえず、コンパイルを通すためにnullを返すようにしたのが、リスト8です。

    public List lookup(String keyword) {
        return null;
    }
リスト 8 PhoneBookクラスのlookupメソッド

これでバーが赤になりました。バーを緑にするために、lookupメソッドの戻り値を空のArrayListオブジェクトにしてみます(リスト9)。これでバーが緑になりました。

    public List lookup(String keyword) {
        return new ArrayList();
    }
リスト 9 PhoneBookクラスのlookupメソッド (フェイクな実装)

とりあえずここはこのままにして、テストを追加します。電話番号を登録してから、検索を行います(リスト10)。PhoneBookクラスは内部的にはマップを使用して情報を保持しているので、検索結果はMap.Entryのリストにしてみました。

    public void testLookup() {
        List results = book.lookup("sakuraba");
        assertEquals(0, results.size());
 
        book.register("Yuichi Sakuraba", "00-1234-5678"); 
        book.register("James Gosling", "+01-555-1234-5678"); 
        book.register("Scott McNealy", "+01-555-0123-4567"); 
 
        results = book.lookup("Sakuraba"); 
        assertEquals(1, results.size()); 
        assertEquals("Yuichi Sakuraba", ((Map.Entry)results.get(0)).getName()); 
        assertEquals("00-1234-5678", ((Map.Entry)results.get(0)).getNumber()); 
 
        results = book.lookup("i"); 
        assertEquals(2, results.size()); 
 
        String name0 = ((Map.Entry)results.get(0)).getValue(); 
        String name1 = ((Map.Entry)results.get(1)).getValue(); 

        if ("Yuichi Sakuraba".equals(name0)) { 
            assertEquals("James Gosling", name1); 
        } else if ("James Gosling".equals(name0)) { 
            assertEquals("Yuichi Sakuraba", name1); 
        } else { 
            fail(); 
        } 
    }
リスト 10 TestPhoneBookクラスのテストを追加したtestLookupメソッド

バーが赤くなったのを確認してから、lookupメソッドを変更しましょう。

マップを使っているので、単に検索キーワードをキーにしてマップから要素を取得すればいいように思います。ところがキーワードがマップのキーと完全に一致しているわけではないはずです。

そこで、マップからすべての要素を取り出してイテレータで1つづつ確かめていくようにしました(リスト11)。

    public List lookup(String keyword) {
        List results = new ArrayList(); 
 
        Iterator it = records.entrySet().iterator(); 
        while (it.hasNext()) { 
            Map.Entry entry = (Map.Entry)it.next(); 
            if (((String)entry.getKey()).indexOf(keyword) >= 0) { 
                results.add(entry); 
            } 
        } 
 
        return results;
    }
リスト 11 PhoneBookクラスのlookupメソッド (トライアンギュレーションな実装)

さて、テストです。もくろみ通り緑になりました。

ところで、検索は名前だけでなく電話番号でも行えるほうが便利です。これもさっそくテストに反映させましょう(リスト12)。

はじめはあいまいだった仕様も、テストを記述しながらだんだんとはっきりしてきました。テストを書く効用はこういうところにもあります。

    public void testLookup() {
        List results = book.lookup("sakuraba");
        assertEquals(0, results.size());
 
        book.register("Yuichi Sakuraba", "00-1234-5678");
        book.register("James Gosling", "+01-555-1234-5678");
        book.register("Scott McNealy", "+01-555-0123-4567");
 
        results = book.lookup("Sakuraba");
        assertEquals(1, results.size());
        assertEquals("Yuichi Sakuraba", ((Map.Entry)results.get(0)).getValue());
        assertEquals("00-1234-5678", ((Map.Entry)results.get(0)).getKey());
 
        results = book.lookup("i");
        assertEquals(2, results.size());
        String name0 = ((Map.Entry)results.get(0)).getValue();
        String name1 = ((Map.Entry)results.get(1)).getValue();
 
        if ("Yuichi Sakuraba".equals(name0)) {
            assertEquals("James Gosling", name1);
        } else if ("James Gosling".equals(name0)) {
            assertEquals("Yuichi Sakuraba", name1);
        } else {
            fail();
        }
  
        results = book.lookup("1234"); 
        assertEquals(2, results.size()); 
        name0 = ((Map.Entry)results.get(0)).getValue(); 
        name1 = ((Map.Entry)results.get(1)).getValue(); 
 
        if ("Yuichi Sakuraba".equals(name0)) { 
            assertEquals("James Gosling", name1); 
        } else if ("James Gosling".equals(name0)) { 
            assertEquals("Yuichi Sakuraba", name1); 
        } else { 
            fail(); 
        } 
    }
リスト 12 TestPhoneBookクラス#testLookupメソッド(番号検索テストを追加)

バーが赤くなるのを確かめたら、緑にするための方策を考えましょう。ここまで行ってくれば、番号の検索を追加するのも簡単です。if 文の条件に番号検索を付け加えるだけです(リスト13)。

このように明らかに実装が分かっている場合はベタに実装してしまいます。このような実装を「明白な実装(Obvious Implementation)」と呼びます。

    public List lookup(String keyword) {
        List results = new ArrayList();
 
        Iterator it = records.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry)it.next();
            if (((String)entry.getKey()).indexOf(keyword) >= 0 
                || ((String)entry.getValue()).indexOf(keyword) >= 0) { 
                results.add(entry);
            }
        }
 
        return results;
    }
リスト 13 PhoneBookクラスのlookupメソッド (明白な実装)

ここでもう一度リファクタリングを行いましょう。さきほど、戻り値のリストの要素にMap.Entryクラスを使用するとしましたが、これは実装の内部に立ち入りすぎている気がします。また、名前をgetValueメソッドで取り出すのも変な感じがします。

そこで、名前と電話番号をペアでもつクラスPhoneRecordを作成しました。本来ならこのクラスのテストもしたいところですが、紙面の都合もあるのでテストは省略します。

このクラスを使用するようにテストを書き換えましょう(リスト14)。

    public void testLookup() {
        List results = book.lookup("sakuraba");
        assertEquals(0, results.size());
 
        book.register("Yuichi Sakuraba", "00-1234-5678");
        book.register("James Gosling", "+01-555-1234-5678");
        book.register("Scott McNealy", "+01-555-0123-4567");
 
        results = book.lookup("Sakuraba");
        assertEquals(1, results.size());
        assertEquals("Yuichi Sakuraba", ((PhoneRecord)results.get(0)).getName());
        assertEquals("00-1234-5678", ((PhoneRecord)results.get(0)).getNumber());
 
        results = book.lookup("i");
        assertEquals(2, results.size());
        String name0 = ((PhoneRecord)results.get(0)).getName();
        String name1 = ((PhoneRecord)results.get(1)).getName();
 
        if ("Yuichi Sakuraba".equals(name0)) {
            assertEquals("James Gosling", name1);
        } else if ("James Gosling".equals(name0)) {
            assertEquals("Yuichi Sakuraba", name1);
        } else {
            fail();
        }
 
        results = book.lookup("1234");
        assertEquals(2, results.size());
        name0 = ((PhoneRecord)results.get(0)).getName();
        name1 = ((PhoneRecord)results.get(1)).getName();

        if ("Yuichi Sakuraba".equals(name0)) {
            assertEquals("James Gosling", name1);
        } else if ("James Gosling".equals(name0)) {
            assertEquals("Yuichi Sakuraba", name1);
        } else {
            fail();
        }
    }
リスト 14 TestPhoneBook#testLookupメソッド(PhoneRecordクラスを使用)

また、同様にPhoneRecordクラスを使用するようにしたlookupメソッドをリスト15に、PhoneRecordクラスをリスト16に示しました。

    public List lookup(String keyword) {
        List results = new ArrayList();
 
        Iterator it = records.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry)it.next();
            if (((String)entry.getKey()).indexOf(keyword) >= 0
                || ((String)entry.getValue()).indexOf(keyword) >= 0) {
                results.add(new PhoneRecord((String)entry.getValue(), (String)entry.getKey()));
            }
        }

        return results;
    }
リスト 15 PhoneRecordクラスを使用したPhoneBook#lookupメソッド

 

public class PhoneRecord {
    private String name;
    private String number;
 
    public PhoneRecord(String name, String number) {
        this.name = name;
        this.number = number;
    }
 
    public String getName() {
        return name;
    }
 
    public String getNumber() {
        return number;
    }
}
リスト 16 PhoneRecordクラス

このように小さな単位でテストとリファクタリングをくり返しながらコードを記述していきます。最終的にPhoneBookクラスとTestPhoneBookクラスはリスト17、リスト18に示したようになりました。

lookupメソッドはもっと効率のいい方法があるかもしれません。しかし、すでにテストを書いてあるので、リファクタリングをしたとしても、ふるまいが正しいかどうかはすぐにテストすることができます。

また、たとえば日本語の処理やソートなど、機能を追加するのであればそれに応じたテストを追加しなくてはいけません。

JUnitを使うことで、アプリケーションは常にテストとともにあることができるのです。

次章ではテストやJUnitの使い方に関するヒントやコツを紹介していきます。

 

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
 
public class PhoneBook {
    private Map records;
 
    public PhoneBook() {
        records = new HashMap();
    }
 
    public boolean register(String name, String number) {
        if (records.containsKey(number)) {
            return false;
        } else {
            records.put(number, name);
            return true;
        }
    }
 
    public List lookup(String keyword) {
        List results = new ArrayList();
 
        Iterator it = records.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry)it.next();
            if (((String)entry.getKey()).indexOf(keyword) >= 0
                || ((String)entry.getValue()).indexOf(keyword) >= 0) {
                results.add(new PhoneRecord((String)entry.getValue(), (String)entry.getKey()));
            }
        }
 
        return results;
    }
 
    public int size() {
        return records.size();
    }
}
リスト 17 PhoneBookクラス

 

import java.util.List;
import java.util.Map;
 
import junit.framework.TestCase;
 
public class TestPhoneBook extends TestCase {
    private PhoneBook book;
 
    public TestPhoneBook(String name) {
        super(name);
    }
 
    public void setUp() {
        book = new PhoneBook();
    }
 
    public void testRegister() {
        assertEquals(0, book.size());
 
        assertTrue(book.register("Yuichi Sakuraba", "00-1234-5678"));
        assertEquals(1, book.size());
 
        // 同じ名前で複数の番号は登録できる
        assertTrue(book.register("Yuichi Sakuraba", "00-0123-4567"));
        assertEquals(2, book.size());
 
        // 同じ番号は複数登録できない
        assertFalse(book.register("James Gosling", "00-1234-5678"));
        assertEquals(2, book.size());
    }
 
    public void testLookup() {
        List results = book.lookup("sakuraba");
        assertEquals(0, results.size());
 
        book.register("Yuichi Sakuraba", "00-1234-5678");
        book.register("James Gosling", "+01-555-1234-5678");
        book.register("Scott McNealy", "+01-555-0123-4567");
 
        results = book.lookup("Sakuraba");
        assertEquals(1, results.size());
        assertEquals("Yuichi Sakuraba", ((PhoneRecord)results.get(0)).getName());
        assertEquals("00-1234-5678", ((PhoneRecord)results.get(0)).getNumber());
 
        results = book.lookup("i");
        checkResult(results);
 
        results = book.lookup("1234");
        checkResult(results);
    }
 
    private void checkResult(List results) {
        assertEquals(2, results.size());
        String name0 = ((PhoneRecord)results.get(0)).getName();
        String name1 = ((PhoneRecord)results.get(1)).getName();
 
        if ("Yuichi Sakuraba".equals(name0)) {
            assertEquals("James Gosling", name1);
        } else if ("James Gosling".equals(name0)) {
            assertEquals("Yuichi Sakuraba", name1);
        } else {
            fail();
        }
    }
}
リスト 18 TestPhoneBookクラス

JUnit以外のテスティングフレームワーク

JUnit以外に様々な言語用のxUnitがあります。例えば、Smalltalk, C++, Visual Basic, Perlなどが提供されています。その他にも.NET FrameworkのためのNUnitや、HTTPUnit, XMLUnitなどプログラミング言語以外のテストもあります。XProgramming.com (http://www.xprogramming.com/software.htm)ではこれら各種言語用のxUnitへのリンクを集められています。

また、JUnitをベースにして作られたテスティングフレームワークもあります。主なものを表2に示しました。

JUnitと組み合わせて使うツールもあります。

市販されているテスティングフレームワークとしては次に示すような製品があります。

JtestとSimpliaは体験版がダウンロードできます。

表 2 JUnitをベースとしたテスティングフレームワーク
サーバサイド
Cactus http://jakarta.apache.org/cactus/ 第3章参照
JUnitEE http://www.junitee.org/ J2EEのテスト
TagUnit http://www.tagunit.org/ カスタムタグのテスト
DB
DBUnit http://dbunit.sourceforge.net/  
GUI
JFCUnit http://jfcunit.sourceforge.net/  
Abbot http://abbot.sourceforge.net/  
Pounder http://pounder.sourceforge.net/  
Jemmy http://jemmy.netbeans.org/  
その他
JUnitPerf http://www.clarkware.com/software/JUnitPerf.html パフォーマンステスト

 

 

 

 

JUnit 編 JUnit 単体テストのスキルを強化する

はじめに

第1章では、TDD を中心にテストの書き方を扱いました。本章では、JUnit を使ってテストをする際に、疑問に思われる点や,問題になりがちな点を取り上げ, それらを解決する手段を解説してきます。

テスト依存症への道

テスト編

JUnitで行うテストはどのようなテストなのか

テストを大別すると次のように分けることができます。

静的テスト ソフトウェアを動作させずに文法などのチェックするテスト。
単体テスト クラスやメソッド単位で、その動作が正しいかチェックを行うためのテスト。ユニットテストとも呼ばれる。
結合テスト 複数のサブシステムやコンポーネントを組み合わせて行うテスト。
機能テスト ソフトウェア全体の機能をチェックするためのテスト。
システムテスト 負荷テストやパフォーマンステストなど最終的なユーザの立場で行うテスト。
回帰テスト ソースの変更に伴うふるまいの変化をチェックするためのテスト。

基本的にはJUnitで行うテストは上記の単体テストに相当します。前半のテストの例でも、テストを行っているのはメソッドが仕様に対して正しく動作しているかということです。

しかし、中には機能テスト的なものも含まれることがあります。たとえば、JUnitがベースになっているHttpUnitなどは機能テストを行うためのテスティングフレームワークです。

また、TDDではリファクタリングが重要な役割を持っていますが、リファクタリングしたときにクラスやメソッドのふるまいが変化しないかチェックするための回帰テストにもJUnitが使用されます。

JUnitを使用してテストを自動化することができるので、リファクタリングなどの変更に伴う副作用を発見することが容易になります。

どのようなテストを書けばいいのか

アプリケーションのふるまいが変化するような点についてチェックすることからはじめてみましょう。たとえば次にあげる項目をチェックするようにします。

多相性はたとえばデザインパターンのStateパターンなどを使用した場合などです。

もう少し一般的にテストをとらえるために、オブジェクトの状態を考えて見ます。オブジェクトの状態は条件やループによって変化すると考えることができます。

UMLではオブジェクトの状態を表わすために状態遷移図を記述しますが、これを使って考えてみましょう。

たとえば、図3のような状態遷移図があったとします。この図では6つの状態があり、それぞれA、B、C、D、E、Fとします。状態Aからは、xが0以上であれば状態B、負数の場合状態Cに遷移します。

状態Dではflagがtrueならば状態Eを経てから状態Fに遷移しますが、falseであればそのまま状態Fに遷移します。

さて、このような場合どこをテストすればいいのでしょうか。まず考えられるのが、すべての状態を通るようなテストです。たとえば、図3ではA->B->D->E->FというパスとA->C->D->E->Fというパスを通るようなテストを行うわけです。

しかし、これでは十分とはいえません。すなわち、DからFへの遷移をテストしていないからです。そこで、たとえばA->C->D->Fというパスのテストを追加します。

すくなくとも、ここで示したようにすべての経路を一度は通るようなテストを行うべきです。

さらに、すべてのパスをテストすることも考えられます。図3の場合

A->B->D->E->F

A->B->D->F

A->C->D->E->F

A->C->D->F

の4通りが考えられます。この例ではすべてのパスを網羅してもたかが知れていますが、複雑な状態遷移を持つ場合すべてのパスをテストするのは不可能な場合もあります。

そこで考えるのがシナリオです。ユースケースを記述するときに、想定している動作を記述しますが、それがシナリオです。そのシナリオにそってテストを行うようにします。

しかし、単にシナリオをテストするだけでなく、状態が変化するときの境界値のテストを行うようにします。たとえば図3では、状態Aから遷移するときにx≧0であれば状態B、それ以外なら状態Cに遷移するので、遷移の境界値であるx=0とx=-1の場合をテストするようにするわけです。

こうすることで全体の動作的には大まかにテストし、重要な部分だけ細かくテストすることが可能になります。

状態遷移図
図 3 状態遷移図の例

 

何をテストすればいいのか分からない

テストを書くことになれていないと何をテストすればいいか迷ってしまうことがよくあります。そんな時はToDoリストを書いてみましょう。紙に書いてもいいし、ポストイットに書いてディスプレイの横に貼るのでもいいです。もちろん付箋紙アプリケーションを使用してもかまいません。重要なのはリストを作ることです。

リストには作りこむ機能を書きだしていきます。どんな順番でも構いません。思いつくままに書いていきましょう。ただし、なるべく細かく機能を分割するようにしましょう。たとえば、電卓のアプリケーションを作るなら四則演算ルーチンと書くのではなく、足し算、引き算、掛け算、割り算、カッコの処理など具体的に書くようにします。

そして、ここで挙げた項目に関してテストを書くようにします。なるべく依存関係の少ない単独でテストできるもの、どのようにコードを書けばいいか明白なものなどから、テストの順番を決めるようにしましょう。

テストが終了したら、リストからその項目を消します。ただし、消しゴムで消さないでください。取り消し線で線を引くだけにしておきましょう。

テストや実装をしている間に新たに思いついたことがあれば、どんどん書きだしていきます。そして、機能だけでなくリファクタリングしたい項目でもなんでも気になることをリストに書きだしていきましょう。

すべての項目が消されたらアプリケーションの実装もテストも終了しているはずです。

いつテストをすればいいのか

ソースコードを変更したら常にテストしましょう。変更することによって副作用が生じているかもしれません。また、変更の副作用が他のクラスにおよんでいるかもしれないので、変更を加えたクラスに関するテストだけでなく関連するテストはなるべく一緒に実行するようにしましょう。

もし、1つのテストに10分以上かかってしまうなら、毎回テストすることをためらってしまうかもしれません。かといってテストしないのは問題です。時間のかかるテストは、テストを行いにくい場合が多いことを示しています。設計を見なおしてよりシンプルな形にリファクタリングすることをお勧めします。

また、テスト自体も10分以上かかるのであれば、テストの組み立てを考えなおしてみた方がいいかもしれません。

テストデータはどうするか

テストコードと実際のコードの書き方は異なります。通常、コードの中にマジックナンバーが入ることはよくないとされていますが、テストコードではどんどんマジックナンバーを埋めこんでしまいます。

テストはコードの仕様や使い方を表わすようになるので、明白にデータを記述したほうが分かりやすくなります。テストを第三者が見たときに分かりやすいように書くことが重要です。

そのためには、入力と期待される結果が明白に対比できるように書くようにします。

リファクタリングはどのように進めればいいのか

機能拡張とリファクタリングは同時に行うべきではありません。つまり、TestRunnerのバーが赤のうちはリファクタリングを行わず、バーが緑になってはじめてリファクタリングを開始します。

機能拡張時にリファクタリングを同時に行ってアプリケーションのふるまいが変化してしまった場合、それが機能拡張によるものなのかリファクタリングの副作用なのか切り分けが難しくなってしまいます。

また、リファクタリングをしたら必ず回帰テストを行って、アプリケーションのふるまいが変化していないことを確かめるようにします。

とはいうものの、リファクタリングのための時間をわざわざ取るのではなく、常にリファクタリングを行うようにします。

最近はリファクタリングツールがIDEに含まれている場合もあり、これらを活用することで容易にリファクタリングを行うことができます。

実際のリファクタリングの手法に関してはバイブルともいえる参考文献(4)を参照ください。

テストが行いにくい場合はどうする

たとえばテストが行いにくいものとして

などがあります。だからといってこれらをテストしないわけにはいきません。

テストが行いにくいのは、テストの対象がアプリケーションの他の部分に依存している場合が多くあります。このような場合、依存している部分をダミーで作ってしまうということがよく行われます。このようなダミーのオブジェクトはモックオブジェクトと呼ばれます。モックオブジェクトについては第3章をご覧ください。

いずれの場合も他との依存をなるべく減らして、対象となるクラスだけで行えるテストを中心にするようにします。

たとえば、GUIの場合はなるべくビューとロジックを分離させ、ロジックのテストをメインにするようにします。

どうしても、ビューの部分のテストが必要なときはJFCUnit (http://jfcunit.sorceforge.net/)などを使用してみることも考えてみましょう。

読者の中にはjava.awt.Robotクラスを使えばいいのではと思われる方もおられるかもしれません。しかし、Robotクラスはなるべく使用をさけるべきです。Robotクラスはマウスカーソルを移動させることや、マウスクリック、キー入力などのイベントを発生させることが可能ですが、ウィンドウの状態までは考慮してくれません。

たまたま、他のアプリケーションのウィンドウが上に重なっていたとしたら、テストが失敗してしまいます。テストは同じ条件で行えば常に同じ結果を得られるようにしておかないと自動テストを行うことができなくなってしまいます。

既存のコードをテストしたい

もし、既存のコードを書いた本人が、それらのコードのテストコード記述してテストを行うのは可能かもしれません。しかし、テストを前提に作られていないので、テストを行えるようにロジックを分解するなどソースを変更しなくてはいけません。

第三者が作成したソースからテストコードを記述するのはさらに難しくなります。ソースに関する十分なドキュメントがそろっていれば可能かもしれませんが、TDDで行われるような小さな単位でドキュメントが整備してあることはなかなかありません。

まずは新規に作成するコードからテストを作成していきましょう。既存のコードはとりあえずそのままにしておきます。そして、リファクタリングなど既存のコードに手を入れる必要がある場合に、変更を行う部分に対して徐々にテストを追加していくようにすればいいのではないでしょうか。

XPでなくても使えるのか

XP (eXtreme Programming)はJUnitの作者であるKent Beckが提唱している開発プロセスです(3)。XPでは 14のプラクティスがあり、これをベースに開発を進めていきます(コラム参照)。TDDもXPのプラクティスの1つになっています。

Kent BeckはXPの他のタスクがTDDを強化し、TDDが他のタスクを強化する例を示しています(2)

とはいうものの、XP以外の開発プロセスにおいてもTDDは有効です。XPのプラクティスをすべて取りいれるのは難しいかもしれませんが、手始めにJUnitを使用してTDDを試してみてはいかがでしょうか。

DbCとの関係は

DbC (Design by Contract)はBertrand Meyerが提唱した設計手法です(5)。DbCではソフトウェアの機能を次の3つの条件を用いた契約として定義します。

事前条件は入力に対する条件で、事後条件は出力に対する条件です。不変条件は入力や出力に関わらず常に成立する条件になります。これらの条件をアサーションを用いてチェックします。

アサーションはプログラム中にコードと一緒に記述されます。Eiffelというオブジェクト指向言語では当初からassertが言語仕様として定義されていました。これに近いものがJ2SE 1.4で取りいれられたAssertionです。

一方、JUnitではアサーションをコードとは別にテストコードとして記述します。テストが別になるので、オブジェクトの状態や入力をさまざまにして出力をテストすることができます。事前条件が満たされている場合に事後条件が成り立つことが確かめられるだけでなく、事前条件を満たさない入力に対して事後条件が成り立たないことも確かめることができます。

JUnitを使用することで、契約に違反していないことをさまざまな角度から検証することができるのです。

JUnit編

たくさんあるテストをまとめたい

テストをまとめるにはTestSuiteクラスを使用します。TestSuiteオブジェクトの生成は、staticなsuiteメソッドの内部で行うようにし戻り値としてTestSuiteオブジェクトを返すようにします。

suiteメソッドの例をリスト19に示しました。TestSuiteクラスにはテストを追加するためにaddTestSuiteメソッドとaddTestメソッドが用意されています。前者はクラスをテストに追加し、後者はオブジェクトをテストに追加します。

オブジェクトを追加する場合はテストクラスのコンストラクタにテストを行うメソッド名を指定します。この場合、指定されたテストメソッドだけがコールされます。これを利用するには次のようにテストクラスに文字列を引数とするコンストラクタをオーバーライドする必要があります。

    public TestPhoneBook(String name) {
        super(name);
    }

これらのメソッドを使用してTestSuiteオブジェクトにテストを追加していきます。TestSuiteに他のTestSuiteを追加することも可能です。

図2の 10. のように、Swing版のTestRunnerではテストのツリーを見ることができます。

suiteメソッドが定義されるクラスはTestCaseクラスの派生クラスである必要はありません。たとえば、JUnitのサンプルのAllTestsクラスはリスト20に示すようにTestCaseクラスの派生クラスではありません。

TestRunnerクラスはリフレクションを利用してsuiteメソッドをコールし、TestSuiteオブジェクトを取得しています。

    public static Test suite() {
        TestSuite suite = new TestSuite();
	
        // クラスを追加
        suite.addTestSuite(TestPhoneBook.class);  
       
        // TestCase オブジェクトを追加
        // この場合、testGetNumber メソッドののみテストされる
        suite.addTest(new TestPhoneRecord("testGetNumber"));
 
        // 他の TestSuite オブジェクトを追加
        suite.addTest(OtherTest.suite());    
            
        // TestSuite オブジェクトを生成して追加
        suite.addTest(new TestSuite(TestClient.class)); 
		
        return suite;
    }
リスト 19 suiteメソッドの例

 

package junit.samples;
 
import junit.framework.*;
 
/**
 * TestSuite that runs all the sample tests
 *
 */
public class AllTests {
 
    public static void main (String[] args) {
        junit.textui.TestRunner.run (suite());
    }
    public static Test suite ( ) {
        TestSuite suite= new TestSuite("All JUnit Tests");
        suite.addTest(VectorTest.suite());
        suite.addTest(new TestSuite(junit.samples.money.MoneyTest.class));
        suite.addTest(junit.tests.AllTests.suite());
        return suite;
    }
}
リスト 20 JUnitに付属するサンプルAllTestsクラス

 

例外をテストするには

正常パスだけでなく、例外もテストしましょう。例外をテストすることはとても重要なことです。

例外のテストは例外を発生させる状態で例外が発生するかどうかをチェックします。例外が発生すればテストは通過しますが、例外が発生しなければ失敗です。

このような場合にはfailメソッドを使用して強制的にテストを失敗させます。たとえば、リスト21に示すようにIndexOutOfBoundsException例外が発生しなければfailさせるようにします。例外は期待されているものなので、expectedとするのがよく行われています。

例外のテストでは想定されている例外をキャッチするようにし、ExcetpionやThrowableをキャッチしないようにしなければなりません。Exceptionでキャッチしてしまうと想定されている例外か、異なる要因による想定外の例外か区別することができなくなってしまいます。

また、一般のテストで例外を想定していない場合は、例外をキャッチせずにJUnitに例外をキャッチさせるようにします。それにはテストメソッドに throws を記述するようにします。

JUnitは例外をキャッチすると、エラーを表示します(図2の7の部分)。

    public void testException() {
        List list = new ArrayList();
           ...  // 省略
        try {
            list.get(100);
            fail();
        } catch (IndexOutOfBoundsException expected) {}
    }
}
リスト 21 例外のテスト

 

テストの初期化、終了処理を1度だけ行いたい

テストごとの初期化、終了処理はsetUpメソッド、tearDownメソッドに記述することは前述しました。

これらのメソッドは1つ1つのテストメソッドがコールされる前に毎回コールされます。

しかし、たとえばソケットやデータベースを使用する場合テストごとに毎回コネクトするのではなく、テスト全体で1度だけコネクトしたい場合もあると思います。

このような場合はjunit.extensions.TestSetupクラスを派生させたクラスのsetUpメソッド、tearDownメソッドに記述するようにします。

TestSetupクラスはデザインパターンのDecoratorパターンで構成されているので、使う場合はテストにかぶせるような感じで使用します。

リスト22にSetupクラスの使用例を示しました。Setupクラスの無名派生クラスを作成し、そこでソケットやストリームを生成しています。テストメソッドは生成したストリームを使用してテストを行っています。

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
 
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.extensions.TestSetup;
 
public class TestStream extends TestCase {
    private static Socket socket;
    private static OutputStream ostream;
    private static InputStream istream;
 
    public static Test suite() {
        TestSetup setup = new TestSetup(new TestSuite(TestStream.class)) {
            public void setUp() throws Exception {
                socket = new Socket("localhost", 9000);
                ostream = socket.getOutputStream();
                istream = socket.getInputStream();
            }
 
            public void tearDown() throws Exception {
                ostream.close();
                istream.close();
                socket.close();
            }
        };
 
        return setup;
    }
 
    public void testWrite() {
        StreamSample sample = new StreamSample();
        sample.write(ostream, "abcdef");
        assertEquals("abcdef", sample.read(istream));
    }
}
リスト 22 TestSetupクラスの使用例

 

同じテストを繰り返し行いたい

メモリリークのテストなど1度実行しただけではなかなか判定を行うことができない場合があります。

同じテストを複数回行う場合、junit.extensions.RepeatedTestクラスを使用します。RepeatedTestクラスを使用するにはコンストラクタで回数を指定します。

リスト23はRepeatedTestクラスを使用した例です。ここではTestPhoneBookテスト全体を10回、TestPhoneBook#testRegisterメソッドを20回実行しています。

    public static Test suite() {
        TestSuite suite = new TestSuite();
 
        // TestPhoneBook クラス全体を10 回テストする
        suite.addTest(new RepeatedTest(new TestSuite(TestPhoneBook.class), 10));
 
        // TestPhoneBook#testRegisterを20 回テストする
        suite.addTest(new RepeatedTest(new TestPhoneBook("testRegister"), 20));
 
        return suite;
    }
リスト 23 RepeatedTestクラスの使用例

 

テストをマルチスレッドで動作させるには

通信を扱うアプリケーションでサーバとクライアントを同時にテストするには別々のプロセスでテストを行えばいいのですが、テストを開始する手順が煩雑になりがちです。せっかくテストの自動実行が可能になっているのですからなるべく手間はかけたくありません。

そこで、単一のプロセスで、それぞれのテストをマルチスレッドで動作させれば開始手順は単純になります。

テストをマルチスレッドで行うために、JUnitではjunit.extensions.ActiveTestSuiteクラスが用意されています。使い方はTestSuiteクラスと同じですが、各テストは別のスレッドで実行されます。

リスト24はサーバ・クライアントで接続を行うテストをマルチスレッドで行っている例です。具体的にはTestServer#testAcceptメソッドとTestClient#testConnectメソッドが別スレッドで実行されます。

    public static Test suite() {
        TestSuite suite = new ActiveTestSuite();
 
        // 2 つのテストは別々のスレッドで実行される
        suite.addTest(new TestServer("testAccept"));
        suite.addTest(new TestClient("testConnect"));
	
        return suite;
    }
リスト 24 ActiveTestSuiteクラスの使用例

 

プライベートなメソッドやプロパティをテストするには

プライベートメソッドをテストする必要がある場合は、本当にテストが必要か検討してみましょう。通常はパブリックなメソッドのテストで十分なはずです。

また、プロパティに関しても同様のことがいえます。プロパティを直接テストしなくても、そのプロパティを操作するメソッドを通じてテストができるはずです。

それでもどうしてもテストが必要という場合はリフレクションを使用してテストを行うことができます。単にリフレクションを使用してもプライベートにはアクセスできないので、FieldクラスやMethodクラスの親クラスであるAccessibleObjectクラスのsetAccessibleメソッドを使用してアクセスできるようにします。

たとえば、リスト25に示したようにプライベートメソッドとプライベートプロパティのみクラスでもリフレクションを使用すればテストすることができます(リスト26)。

public class PrivateSample {
    private int counter;
    
    private void increment() {
        System.out.println("increment");
        counter++;
    }
}
リスト 25 プライベートメソッドのみを持つクラス

 

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
 
import junit.framework.TestCase;
 
public class TestPrivateSample extends TestCase {
    public void testIncrement()
             throws NoSuchFieldException, NoSuchMethodException,
                    InvocationTargetException, IllegalAccessException {
        PrivateSample sample = new PrivateSample();
        Class sampleClass = sample.getClass();
 
        // プライベートプロパティを取得
        Field field = sampleClass.getDeclaredField("counter");
        // アクセス可能にする
        field.setAccessible(true);
        // 値を取得して、テスト
        assertEquals(0, field.getInt(sample));
 
        // プライベートメソッドを取得
        Class[] params = new Class[]{};
        Method method = sampleClass.getDeclaredMethod("increment", params);
        // アクセス可能にする
        method.setAccessible(true);
        // メソッドコール
        method.invoke(sample, null);
 
        // 値を取得して、テスト
        assertEquals(1, field.getInt(sample));
    }
}
リスト 26 プライベートへのアクセスを行うテストケース

AntからJUnitを使いたい

AntにはJUnitを行うためのJUnitタスクが用意されています。mypackage.TestSomeを行う場合には次に示すように<junit>タグの中に<test>タグを使用してテストを指定します。

<junit>
  <test name="mypackage.TestSome" />
</junit>

JUnitタスクにはさまざまなオプションがあるので、詳細はAntのドキュメントを参照してください。

ただし、JUnitタスクによって起動されるTaskRunnerはテキストベースのものです。GUIベースのTestRunnerを起動するには<java>タスクでTestRunnerを起動させます。

<java fork="yes" classname="junit.swingui.TestRunner">
    <arg value="mypackage.TestSome"/>
</java>

Ant以外にも、Eclipseに代表されるIDEなどさまざまツールでJUnitと連携できるものが多くあります。

おわりに

JUnitを使用してテストを書くことははじめのうちはとても煩雑に思えるかもしれません。直接コードを書きたくでてうずうずしているのに、「ちょっと待て、テストが先だ」なんて。

それでも、テストを書くことによってソフトウェアの仕様を確認することができます。筆者はテストを書いているときに、「あっ、あれを忘れていた」と思い出すことがよくあります。そして、リストに書き出すのです。

最終的にTDDでソフトウェアを作成することで、質の向上が行えます。ぜひ、テストを書くことが習慣になってしまうまでがんばってみましょう。

最後になりましたが、本解説を執筆するにあたり横河電機 山川利治氏に有益な議論やコメントをいただきました。この場を借りて感謝いたします。

eXtreme Programing

アジャイルな開発プロセスがおおはやりですが、その代表格がXPです。もともと、Kent Beckが携わったクライスラーのC3プロジェクトでの経験をもとにまとめあげられた開発プロセスです。

それまでの開発プロセスでは変更のためのコストが開発後期になればなるほど高くなるとされていました。しかし、XPの合言葉「変化ヲ抱擁セヨ (Embrace Change)」に示されるように、変更することでプログラムを成長させていくという特徴をもちます。これを実現するためにXPでは14のプラクティスが示されています。

計画ゲーム オンサイト顧客
共同所有 テスティング
小さなリリース コーディング規約
継続した統合 リファクタリング
メタファー オープンワークスペース
週40時間労働 ペアプログラミング
シンプルな設計 デイリースキーマインテグレーション

 

参考文献

1. 北野 弘治、平鍋 健児 「車窓からのTDD テスト駆動開発の進め方」 JAVA PRESS Vol.33, 技術評論社
2. Kent Beck 「テスト駆動開発入門」 ピアソン・エデュケーション、ISBN 4-89471-711-5
前半がTDDを利用したアプリケーション構築例、後半がTDDに関する さまざまなパターンやトピックを取り上げています。懇切丁寧にTDDを解説してくれます。
3. Kent Beck 「XPエクストリーム・プログラミング入門」 ピアソン・エデュケーション、ISBN 4-89471-275-X
薄い本ですらすら読めますが、内容は深いです。表紙が白いところから白本と呼ばれています。緑本(XPエクストリーム・プログラミング実行計画)などもご一緒に。
4. Martin Fowler 「リファクタリング プログラミングの体質改善テクニック」ピアソン・エデュケーション、ISBN 4-89471-228-8
Martin Fowlerはアナリシス・パターンのような上位層からリファクタリングなどの実装に関するものまで幅広く手がけています。どれもおもしろく、Fowlerに外れなしと個人的に思ってます。
5. Bertrand Meyer 「オブジェクト指向入門」 アスキー ISBN 4-7561-0050-3
DbCについて学ぶならやはり原典を外すことはできません。