第5回
EclipseとJUnitによるテスティング(2)
テストファーストでコードを作成する

縣俊貴
橋本正徳
Project Mobster/メディアファイブ株式会社

2003/4/11
前回「EclipseとJUnitによるテスティング」は「テストの意義」と「テスティングフレームワーク」に焦点を絞って解説しました。今回は実際にEclipseを使ってテストファーストでコードが作られていく様子をチュートリアル形式でご紹介します。JUnitを用いた単体テストはXP開発でなくてもシステムの品質向上に十分に役に立つプラクティスです。ぜひJUnitの持つパワーを体験してみてください

EclipseでJUnitを使ってみよう

 EclipseにはすでにJUnitによるテスト実行環境が備わっています。そのほかにもCVSやリファクタリングと、まさにXPのための開発環境だといえます。それでは、Eclipseでテストファーストをやってみましょう。Enjoy Testing!

EclipseでJUnitを使う準備

 まずはプロジェクトのビルドパスにjunit.jarを追加します。ビルドパスの追加にはいくつかの方法があります。ここでは2つの方法をご紹介します。どちらかの方法でjunit.jarをビルドパスに追加してください。

(1)クラスパス変数「JUNIT_LIB」を定義する方法

 jarファイルやディレクトリに対するエイリアスをクラスパス変数として定義することができます。一度定義してしまえば、どのプロジェクトからでも呼び出すことができ、また最新版のjarファイルへの変更なども一発でできて便利です。ここではjunit.jarへのエイリアスとして、クラスパス変数「JUNIT_LIB」を定義します。junit.jar自体はEclipseのインストールディレクトリ直下の「plugins」フォルダの「org.junit_3.7.0」にあります。

◎クラスパス変数「JUNIT_LIB」の定義

  1. [ウィンドウ]-[設定]

  2. [Java]-[クラスパス変数]-[新規]

    新規変数エントリダイアログが表示されます。

    図1 新規変数エントリ

  3. 必要事項を記入します

    名前:JUNIT_LIB
    パス:(junit.jarまでの絶対パス)

    図2 クラスパス変数設定(クリックすると拡大)

  4. [OK] - [OK]

◎ビルドパスにクラスパス変数「JUNIT_LIB」を追加

  1. [プロジェクトの上で右クリック]-[プロパティ]-[Javaのビルド・パス]-[ライブラリ]-[変数の追加]

    新規変数クラスパス・エントリダイアログが表示されます。

  2. 「JUNIT_LIBを選択」-[OK]-[OK]

    これでプロジェクトのビルドパスにjunit.jarを追加することができました。
図3 JUNIT_LIBの追加

(2)プロジェクト内にjunit.jarをインポートする方法

 プロジェクトに「lib」フォルダを作り、そこに「junit.jar」をインポートします。インポートしたjarファイルをビルドパスに追加してください。配布時に「junit.jar」を含みたいときなどに便利だと思います。

■Eclipseによるテストファーストチュートリアル

 さて、ここではEclipseとJUnitを使って、テストファーストを体験していただきたいと思います。短いチュートリアルですが、単体テストやテストファーストのメリット、つまり「『テストがある』状態ならコードの変更も安心して行える」「最終的には品質向上につながる」などを実感していただければと思います。ではさっそくチュートリアルの始まりです。Enjoy Testing!

テストコードは仕様であり、ドキュメントだ!
 テストファーストで書かれるコードは仕様であり、ドキュメントです。テストコードを見れば、テスト対象のクラス・メソッドの振る舞い、使い方を知ることができます。また、テストコードはテスト対象のコードとともに、自然に成長し、更新されていきます。そのためにもテストコードにはコメントをなるべくたくさん書きましょう。また、テストコードは冗長な部分があってもいいので、分かりやすい、シンプルなメソッドの使い方を表す書き方をしましょう。引き継ぎやシステムの機能拡張のとき、本当の意味でテストコードのありがたみを知ることになります。

■テスト対象クラスの機能

 このチュートリアルで作成する機能は、文字列の分割(split)メソッドです。これは、例えば文字列“a,b,c”と区切りカンマ“,”を引数として渡すと、カンマで文字列を分割してString型の配列[“a”][“b”][“c”]を返してくれるものです。

 JDKの1.4から、Stringクラスに正規表現によるsplitメソッドが実装されていますが、JDK 1.4以前でも同じような処理を行うことができるように、ユーティリティメソッドとしてsplitを作成してみたいと思います。

文字列=“a,b,c”、区切り文字=“,” 結果のString型の配列=[“a”][“b”][“c”]
文字列=“mobster<BR>project”、区切り文字=“<BR>” 結果のString型の配列=[“mobster”][“project”]
splitメソッドによる分割の例

1.テストコードの作成

 プログラミングの最初のステップはテストコードの記述です。Eclipseではテスト対象クラスから、テストクラスを作成する機能があります。次の手順でテスト対象クラスとテストクラスのひな型を作ります。

<<テスト対象クラスのひな型を作成する>>

 ここでは「StringUtil」クラスを新規作成します。

  1. [ファイル]-[新規]-[クラス]

    Javaクラスウィザードが表示されます。

  2. [必要項目を入力]-[終了]

    プロジェクト:(プロジェクト名入力、もしくは「ブラウズ」で選択)
    パッケージ:空(デフォルト・パッケージ)
    名前:StringUtil
    スーパークラス:java.lang.Object
    チェックボックス:すべて外す
図4 空のクラス作成(クリックすると拡大)

<<テストクラスを作成する>>

 テスト対象のStringUtilからJUnit用のテストクラスを作成します。

  1. [StringUtilの上で右クリック]-[新規]-[その他]

    新規・選択ウィザードが表示されます。

    図5 [新規]-[その他](クリックすると拡大)

    図6 新規・選択(クリックすると拡大)

  2. [Javaを展開]-[JUnit]-[TestCase]-[次へ]

    新規JUnit TestCase作成ウィザードが表示されます。

  3. [必須項目を入力]-[終了]

    ソースフォルダ:(プロジェクト名入力、もしくは「ブラウズ」で選択)
    パッケージ:空(デフォルト・パッケージ)
    テストケース:StringUtilTest
    テストクラス:StringUtil
    スーパークラス:junit.framework.TestCase
    チェックボックス:すべてチェック
    TestRunnerステートメントの追加:(Eclipseを使わない人のために選択しておく)
図7 新規JUnitTestCase(クリックすると拡大)

 これでテスト対象クラス、テストクラスのひな型が完成しました。では「StringUtilTest」にテスト用のコードを記述します。テストコードを記述するメソッド名は、testから始まる名前にする必要があります。この[testXXX]というメソッドを、JUnitのフレームワークがJavaリフレクションAPIを利用して呼び出すことでテストが実行されます。メソッド名はテスト対象のメソッド名を後ろに付けると分かりやすいです。ここではtestSplit()というメソッドを作成します。

StringUtilTest(その1)
import junit.framework.TestCase;

/*
 *TestCaseを継承したStringUtilのテストケース
 */
public class StringUtilTest extends TestCase {


        public StringUtilTest(String arg0) { super(arg0); }


        /**
    * 文字列分割のテスト
    */
        
        public void testSplit() {
        }
}

 テストファーストの場合、クラスやメソッドの使い方を中心にコーディングを行います。まずはsplitメソッドの使い方を考えてみましょう。文字列を指定された区切り文字列で分割するので、引数は文字列と区切り文字列の2つでしょう。StringUtil#splitメソッドはまだ存在しませんが、「あるもの」としてコーディングします。戻り値は分割された結果がString型の配列で返されるはずです。よってテストコードは次のようなものが考えられます。まずはこのテストコードを記述します。

StringUtilTest#testSplit
   public void testSplit() {
   // 分割対象の文字
   String str = "a,b,c";

   // カンマで文字列を分割
   String[] result = StringUtil.split(str, ",");

   // 以下結果を確認 -----------------------------------------    

   // resultはnullではないはず
   assertNotNull(result);    //

   配列の長さは3のはず
   assertEquals(3, result.length);

   // 分割結果が配列に格納されているはず
   assertEquals("a", result[0]);
   assertEquals("b", result[1]);
   assertEquals("c", result[2]);
   }

 テストコードの中で「assertEquals()」というメソッドが呼ばれています。このメソッドはTestCaseの親クラスAssertの持っている判定用メソッドで、第1引数と第2引数をObject#equals()で比較してtrueのときに判定成功となります。失敗した場合、例外が返されて、このテストメソッドのテストは終了します。このようにメソッドの戻り値やオブジェクトの状態を判定することによって、期待した振る舞いをテストしていきます。

メソッド 判定 メッセージ
assertEquals(プリミティブ型 arg1, プリミティブ型 arg2) expected、arg2の値が同じ値 なし
assertEquals(String message, プリミティブ型 arg1, プリミティブ型 arg2) arg1、arg2の値が同じ値 あり
assertEquals(Object arg1, Object arg2) arg1.equals(arg2)で比較してtrue なし
assertEquals(String message, Object arg1, Object arg2) arg1.equals(arg2)で比較してtrue あり
assertTrue(boolean arg1) arg1がtrue なし
assertTrue(String message, boolean arg1) arg1がtrue あり
assertFalse(boolean arg1) arg1がfalse なし
assertFalse(String message, boolean arg1) arg1がfalse あり
assertNull(Object arg1) arg1がnull なし
assertNull(String message, Object arg1) arg1がnull あり
assertNotNull(Object arg1) arg1がnullではない なし
assertNotNull(String message, Object arg1) arg1がnullではない あり
assertSame(Object arg1, Object arg2) arg1 == arg2がtrue なし
assertSame(String message, Object arg1, Object arg2) arg1 == arg2がtrue あり
assertNotSame(Object arg1, Object arg2) arg1 == arg2がfalse なし
assertNotSame(String message, Object arg1, Object arg2) arg1 == arg2がfalse あり
fail() テスト強制失敗 なし
fail(String message) テスト強制失敗 あり
表:Assertメソッド

 テストコードが完成したら保存してコンパイルしてみましょう。もちろん、StringUtilにはsplitメソッドがないのでコンパイルエラーが出ます。ここではEclipseの未作成メソッドの自動生成機能を使って空のsplitメソッドを作成してみましょう。

<<未作成メソッドの自動生成機能>>

  1. [CTRL+s でテストコードを保存・コンパイル]

    ソース中にコンパイルエラーを示す電球マーカーが表示されます。

  2. [電球マーカーをクリック]-[split(..)' を StringUtil に作成]を選択

    StringUtilクラスに空のsplitメソッドが作成されます。保存するとStringUtilTestのコンパイルエラーがなくなります。これでひとまずテストクラスが完成しました。
図8 電球マーカー(クリックすると拡大)

図9 メソッド自動生成(クリックすると拡大)

2.テストの実行(失敗)

 実装コードは記述していませんが、テストを実行してみます。実装コードがありませんので、テストは失敗しますが、これは「テストが必ず失敗する」ことを確認するのが目的です。これで成功した場合、テストがきちんと作成されていない可能性があります。テストの実行は次の手順で行います。

ペアプログラミングとユニットテスト
 ペアプログラミングとユニットテストを組み合わせることで、1人でテストコードを書くよりも、より良いテストコードを書くことができます。テストを書くということは、仕様を明確にして、設計を行っていきます。この作業をペアで行えば1人でやるより、仕様的に必要なもの必要でないものの切り分けがうまくいきます。また、テストを使うことで心配なのは、そのテストコードの質です。1人で書いた謎(なぞ)のテストコードより、2人で書いた確かなテストコードの方がいいのは当たり前です。また、コードの共同所有を行うことで、テストコードも「たくさんの目」に触れることになり、さらに強固なテストコードが出来上がっていくことになります。

<<JUnitテストの実行>>

  1. [パッケージ・エクスプローラ]上の[StringUtilTest]をダブルクリックしてソースビューにソースを表示する

  2. [実行]-[次を実行]-[JUnitテスト]を選択

 実行すると、StringUtilの空のsplitメソッドはnullを返す実装なので、

// resultはnullではないはず
assertNotNull(result);

 この条件でテストは失敗し、画面上には失敗を表す「赤いバー」がのびます。

図10 テスト失敗

3.実装コードの記述

 では実装コードを記述します。java.util.StringTokenizerを使って分割処理を記述してみます。次のコードをStringUtilに追加します。

StringUtil(その1)
public class StringUtil {
        /**
         * 文字列の分割(区切り文字指定)
         * @param str 対象文字列
         * @param delim 区切り文字列
         * @return 分割後の文字列
         */
        public static String[] split(String str, String delim) {
                StringTokenizer st = new StringTokenizer(str, delim);
                int length = st.countTokens();
                String[] result = new String[length];
                for (int i = 0; i < length; i++) {
                          result[i] = st.nextToken();
                }
                return result;
        }
}   

4.テストの実行

 実装が済んだらもう一度テストを実行します。今度は、ちゃんと期待どおり動くように実装しているので成功を示す「緑のバー」がのびます。

図11 テスト成功

5.さらにテストコードを追加してみる

StringUtilTest#testSplit 2
   public void testSplit2() {
   String str = null;
   String result[] = null;


   // 別の文字列を指定
   str = "A*B*C";
   result = StringUtil.split(str, "*");
   assertNotNull(result);
   assertEquals(3, result.length);
   assertEquals("A", result[0]);
   assertEquals("B", result[1]);
   assertEquals("C", result[2]);
   

   // 長い区切り文字
   str = "123[SEP]456[SEP]789";
   result = StringUtil.split(str, "[SEP]");
   assertNotNull(result);
   assertEquals(3, result.length);
   assertEquals("123", result[0]);
   assertEquals("456", result[1]);
   assertEquals("789", result[2]);
   }
 

6.さらにさらにテストコードを追加……実装コードの変更が発生した!

 さらに実装コードの変更を伴うテストコードを追加してみます。下記のテストコードでは、分割対象の文字列に空文字が含まれています。Splitメソッドの仕様としては、空文字も分割の結果に含まれるのを正しい動きとして、テストコードを記述してみました。このテストを実行してみてください。結果は失敗になります。StringTokenizerクラスでは区切り文字間の区切り文字が空文字の場合、結果のトークンに含まれない仕様となっているからです。

StringUtilTest#testSplit 3
   public void testSplit3() {
   // 分割対象の文字(空文字列あり)
   String str = "a,,";


   // カンマで文字列を分割
   String[] result = StringUtil.split(str, ",");


   // 以下結果を確認 -----------------------------------------

   // resultはnullではないはず
   assertNotNull(result);
   

   // 配列の長さは3のはず
   assertEquals(3, result.length);
   

   // 分割結果が配列に格納されているはず
   assertEquals("a", result[0]);
   assertEquals("", result[1]);
   assertEquals("", result[2]);


   }
    

 よってStringUtilの実装コードをStringTokenizerを用いない処理に大幅に変更してみます。

StringUtil(その2) StringTokenizerを用いない場合
public class StringUtil {
        /**
         ・ 文字列の分割(区切り文字指定)
         ・ @param str 対象文字列
         ・ @param delim 区切り文字列
         ・ @return 分割後の文字列
         */
        public static String[] split(String str, String delim) {
                final int delimLength = delim.length();
                int pos = 0;
                int index = 0;
                ArrayList list = new ArrayList();

                while((index = str.indexOf(delim, pos)) != -1) {
                        list.add(str.substring(pos, index));
                        pos = index + delimLength;
                }
                list.add(str.substring(pos));

                return ( (String[])list.toArray(new String[0]) );
        }
}   

 再度テストを実行します。いままでのテストがすべて行われて成功するはずです。単体テストがあればこのように「変更前の状態が正しいこと」「変更が正しいこと」を確認できます。しかもこのテストはボタン1つで「いつでも・何度でも・自動的に」実行することができます。「動くものにも触れることができる」状態を保っていくためにも単体テストをきちんと作っていくことをお勧めします。

<<TestSuiteでテストをまとめて実行>>

  1. [(デフォルト・パッケージ)の上で右クリック]-[新規]-[その他]

    新規・選択ウィザードが表示されます。(普通はTestSuiteを作りたいパッケージの上で右クリックすることになります)

  2. [Javaを展開]-[Junit]-[TestSuite]-[次へ]

    新規 Junit TestSuite作成ウィザードが表示されます。

  3. [必須項目を入力]-[終了]

    ソースフォルダ:(プロジェクト名入力、もしくは「ブラウズ」で選択)
    パッケージ:空(デフォルト・パッケージ)
    テストケース:StringUtilTest
    テスト・スイート:AllTests
    スイートに含まれるテストクラス:(スイートに含まれるテストクラスをチェック)
    チェックボックス:すべてチェック
    TestRunnerステートメントの追加:(Eclipseを使わない人のために選択しておく)
図12 JunitTestSuite(クリックすると拡大)

AllTests.java
import junit.framework.Test;
import junit.framework.TestSuite;


/**
 * デフォルト・パッケージのTestSuite
 */
public class AllTests {

         public static void main(String[] args) {
                  junit.swingui.TestRunner.run(AllTests.class);
         }

         public static Test suite() {
                  TestSuite suite = new TestSuite("Test for default package");
                  //$JUnit-BEGIN$
                  suite.addTest(new TestSuite(StringUtilTest.class));
                  suite.addTest(new TestSuite(OtherTest.class));
                  suite.addTest(new TestSuite(ArrayUtilTest.class));
                  //$JUnit-END$
                  return suite;
         }
}

 今回は、テストファーストプログラミングを体験してみました。テストのパワーを感じて頂けたのではないかと思います。車の運転と同じで、「体験」のあとは「実践」することが大切です。ぜひ、開発の現場でテストファースト/ユニットテストを実践して欲しいと思います。次の段階では、プロジェクトリーダーやチームメンバーにテストの必要性を伝えていきましょう! チームで実践してこそ、テストのメリットが生きてきます。次はあなたがテストの伝道師となる番です。そのときは、ぜひ本マニュアルをご活用下さい。

 さて、次回のテーマは、リファクタリングです。リファクタリングの基本的な考え方と、Eclipseに備わる強力なリファクタリング機能を合わせてご紹介します。お楽しみに!


プロフィール
縣俊貴(あがた としたか)
 メディアファイブ株式会社所属。XML,フレームワークを中心に開発業務に携わる。Javaのコミュニティー団体であるMobsterを主催。現在MonsterにてJavaベースのWikiシステム「MobWiki」を開発している。

橋本正徳(はしもと まさのり)
 メディアファイブ株式会社所属。XML、フレームワーク等の開発業務に携わる。Javaのコミュニティー団体である「Mobster」を縣と共に発起、運営。現在mobsterにてバグトラッキングシステム「mobbug」等を開発している。「日本XPユーザーグループ関西支部 九州分科会」にも参加。ちなみにこの記事自身もCVSでバージョン管理し、縣と橋本とで共同所有されて書かれている。

Project Mobster(ぷろじぇくと もぶすたー)
福岡県福岡市を中心にJava言語を研究追求し、その成果物をWeb上に公開していく団体です。年齢・スキル・会社などを超えてボーダーレスに活動しております。

IT Architect 連載記事一覧

この記事に対するご意見をお寄せください managemail@atmarkit.co.jp

「ITmedia マーケティング」新着記事

「AIによる顧客体験(CX)向上」に懐疑的な見方が広がる――Qualtrics調査
Qualtricsが実施した年次グローバル調査から見えたカスタマーエクスペリエンス(CX)の現...

2025年のSNS大予測 AIの時代に「ソーシャル」はどう変わる?
もういくつ寝ると2025年……と数えるのはさすがに気が早いかもしれないが、それでも2024...

SEOで陥りがちな失敗は「アルゴリズム変更に対応できなかった」が最多に 原因は?
SEOの成功には何が必要なのか、失敗経験者へのアンケートで浮き彫りになったこととは……。