JUnitフレームワークによるJava ME単体テストはじめに単体テストは、Java Standard Edition(SE)およびEnterprise Edition(EE)の大部分のアプリケーション開発の基本要素になっています。テスト駆動型の環境を採用しているアプリケーションでは特にそうです。Kent BeckとEric GammaのオリジナルのSmalltalkフレームワークは大いに人気を博して成功を収めたので、さまざまなプログラミング言語に移植されました。その中には、たとえばAda(AUnit)、C#(NUnit)、Python(PyUnit)、Fortran(fUnit)などがあります。おそらく、JavaのJUnitは最も成功した単体テストフレームワークでしょう。そして、拡張という形で多数の子孫を生み出しており、マルチスレッドJavaアプリケーションから強力なエンタープライズJavaアプリケーションに至るまで、あらゆるものを単体テストするのに役立っています。 とはいえ、Java Micro Edition(ME)開発にJUnitを使用したり、そのためのJUnit拡張を見つけたりするのは少々大変でした。JUnitフレームワークはJavaリフレクションに依存しています。Java ME環境にはリフレクションAPIがないので、リフレクションへの依存度が高い典型的なJUnitツールは役に立ちません。しかしこの悪条件にもかかわらず、特にデバイスアプリケーション開発者向けに作られたJava ME JUnit拡張が2つ公開されています(後ほど触れますが、近いうちに1つになります)。さらに重要なのは、NetBeansとNetBeans Mobility Packにおいて、Java ME JUnitスタイルのフレームワークをNetBeansおよびNetBeans Mobility 5.5のリリースと合体させる計画があることです。このIDEでは、MEアプリケーションに単体テストをすばやく追加できるようになります。 本稿では、Java ME JUnitフレームワークを使ったJUnitテストを紹介します。このフレームワークをどこから入手するか、どうやってテストを実施するか、どのように使えば質の高いソフトウェアを構築できるかを説明します。 Java ME単体テストフレームワークの入手現時点で入手できるJava ME単体テストフレームワークは、J2MEUnitとJMUnitの2つです。どちらのプロジェクトもオープンソースフレームワークであり、SourceForge.netから1つのダウンロードファイルで入手できます。 ただし、この2つのオープンソースプロジェクトのプロジェクト管理者であるElmar Sonnenschein(J2MEUnit)とBrunno Silva(JMUnit)は、2つのフレームワークを合体させて1つにまとめるつもりです。新しいプロジェクトはJ2MEUnitプロジェクトの下に統合されます。Sonnenscheinによると、「ユーザーベースが大きくなるので、新しいプロジェクトはSourceForgeでJ2MEUnitという名前の下にホストされることになる。我々はBrunnoのJMUnit 2.0コードベースからJ2MEUnit 2.0を作成するつもりだ」。SonnenscheinはSilvaとの最近のやりとりの中で、2.0リリースと合体後の製品の完成が今年中には間に合わないだろうと言っています。合体版が提供されるまでMEアプリケーションでの単体テストの利用を先送りにするという手もありますが、先に取り組んでみたらどうでしょうか。さらにSilvaは次のように言っています。「これらのプロジェクトでは両フレームワークの現在のユーザーのコードを無駄にしたくないので、元のコードを提供する予定だが、できるだけ減らす方向で進めている。合体したフレームワークはJMUnitとJ2MEUnitの両方の機能を持つことになるはずだ」。 単純なアプリケーション種々の単体テストフレームワークを調べるには、テスト対象となる単純なコードが必要になります。今回は、下記の単純な変換クラスを使って、Java MEの単体テストを作成しテストすることにします。 public class DistanceConversion { public static int feetToMeters(int ft) { return (ft * 3048) / 10000; } public static int metersToFeet(int meters) { return (meters * 3281) / 1000; } public static int milesToKM(int miles) { return (miles * 1609) / 1000; } public static int kmToMiles(int km) { return (km * 6214) / 10000; } } public class TemperatureConversion { public static float fahrenheitToCelsius(float degrees) { return ((degrees - 32) / 9) * 5; } public static float celsiusToFahrenheit(float degrees) { return ((degrees * 9) / 5) + 32; } public static boolean isHotter(float degFaren, float degCel) { return ((fahrenheitToCelsius(degFaren)) - degCel) > 0; } public static boolean isCooler(float degFaren, float degCel) { return ((fahrenheitToCelsius(degFaren)) - degCel) < 0; } } このコードではCLDC 1.1の浮動小数点基本型を使用しています。このコードをCLDC 1.0環境で実行するためには、浮動小数点基本型に代わって、下記のように整数基本型を使用する必要があります。本稿のダウンロードサンプルでは、CLDC 1.1用とCLDC 1.0用の両方のコードとテストクラスをzipファイルで提供しています。 public class DistanceConversion { public static int feetToMeters(int ft) { return (ft * 3048) / 10000; } public static int metersToFeet(int meters) { return (meters * 3281) / 1000; } public static int milesToKM(int miles) { return (miles * 1609) / 1000; } public static int kmToMiles(int km) { return (km * 6214) / 10000; } } public class TemperatureConversion { public static int fahrenheitToCelsius(int degrees) { return ((degrees - 32) / 9) * 5; } public static int celsiusToFahrenheit(int degrees) { return ((degrees * 9) / 5) + 32; } public static boolean isHotter(int degFaren, int degCel) { return ((fahrenheitToCelsius(degFaren)) - degCel) > 0; } public static boolean isCooler(int degFaren, int degCel) { return ((fahrenheitToCelsius(degFaren)) - degCel) < 0; } } JMUnitでの作業JMUnitのセットアップJMUnitダウンロードを入手したら、Java MEコンパイラとランタイム環境またはIDEの両方について、2つのJMUnit .jarファイル(「JMUnit4CLDC10.jar」と「JMUnit4CLDC11.jar」)をクラスパス上に配置します。JMUnitの現在のリリースは1.0.2です。 JMUnitのテストケース JMUnitが提供するフレームワークには2つのバージョンがあり、別々のJARに収められています。1つはCLDC 1.0アプリケーション用で、もう1つは浮動小数点基本型をサポートするCLDC 1.1アプリケーション用です。JUnit流のしきたりでは、JMUnitを使用する適切な単体テストを作成するための最初のステップは、テストケースを作成することです。JMUnitのテストケースを作成するには、JMUnitのjmunit.framework.cldc10.TestCaseまたはjmunit.framework.cldc11.TestCaseを拡張した新しいテストケースクラスを作成しなければなりません。パッケージ名からわかるように、前者は1.0バージョンのサポートを提供し、後者は1.1バージョンのサポートを提供します。両者の違いは、cldc11.TestCaseで実装されている JUnitのしきたりでは、テストケースクラスの名前はテスト対象クラスの名前を含み、末尾が「Test」でなければなりません。したがって、先ほどの温度変換クラスのCLDC 1.1バージョンをテストするJMUnitテストケースなら、次のように定義されるでしょう。 public class TemperatureConversionTest extends jmunit.framework.cldc11.TestCase {} すべてのテストメソッドがテストケースクラスに含まれていなければなりません。JUnitのしきたりにより、テストメソッドの名前は「test」で始まり、その後ろにテスト対象クラス内のメソッドの名前が付きます。たとえば、 assertTrue(expression) assertFalse(expression) assertSame(expected, actual) assertNotSame(expected, actual) assertEquals(expected, actual) assertNotEquals(expected, actual) assertNull(object) assertNotNull(object) JMUnitでは、これらのアサーション呼び出しのいずれかを使用するテストメソッドはAssertionFailedExceptionをスローしなければなりません。フレームワークは、この例外を使って失敗したテストを識別します。適切なテストメソッドを持つTemperatureConversionTestクラスは、次のようになるでしょう。 import jmunit.framework.cldc11.*; public class TemperatureConversionTest extends TestCase { public void testfahrenheitToCelsius() throws AssertionFailedException { System.out.println("fahrenheitToCelsius"); float result = TemperatureConversion.fahrenheitToCelsius(66F); assertEquals(18.88889F, result); } public void testcelsiusToFahrenheit() throws AssertionFailedException { System.out.println("celsiusToFahrenheit"); float result = TemperatureConversion.celsiusToFahrenheit(20F); assertEquals(68F, result); } public void testisHotter() throws AssertionFailedException { System.out.println("isHotter"); assertTrue(TemperatureConversion.isHotter(70F, 2F)); } public void testisCooler() throws AssertionFailedException { System.out.println("isCooler"); assertTrue(TemperatureConversion.isCooler(10F, 10F)); } } 標準のJUnit実装により、JMUnitのテストケース抽象クラスには JMUnitのテストケースクラスにはコンストラクタも付属しています。JMUnitのテストケースクラスのいずれかを拡張したテストケースクラスのコンストラクタは、スーパーコンストラクタを呼び出し、テストケースのテスト数を表す整数とテストケースを識別する文字列を渡さなければなりません。 public TemperatureConversionTest() { super(4, "TemperatureConversionTest"); } テスト数を表す整数は、テストケースの実際のテスト数と一致していなければなりません。コンストラクタに渡すテスト数と、テストケースの実際のテスト数とを一致させることが大切です。テストケースの テストケースの public void test(int testNumber) throws Throwable { switch(testNumber) { case 0:testfahrenheitToCelsius();break; case 1:testcelsiusToFahrenheit();break; case 2:testisHotter();break; case 3:testisCooler();break; default: break; } } これがテストケースコンストラクタにテスト数を渡さなければならない理由です。実行時、JMUnitフレームワークはテストケースクラスのインスタンスを作成します。フレームワークはループの中で、0から(テスト数−1)まで、テストケースインスタンスの各メソッドを呼び出します。このようにして、テストメソッドの JMUnitでは開発者自身がテストメソッドを書くので、テストメソッドの実行方法がJUnitとは異なってきますし、おそらくJMUnitの方が柔軟性に優れています。JMUnitではテストメソッドを制御できるため、パラメータを受け取るテストを書くことができます(JUnitではリフレクションを利用するのでこれができません)。たとえば、TemperatureConverstionTestのテストメソッドは次のようになるでしょう。 public void testcelsiusToFahrenheit(float c, float f) throws AssertionFailedException { System.out.println("celsiusToFahrenheit(float c)"); float result = TemperatureConversion.celsiusToFahrenheit(c); assertEquals(f, result); } こうすると、テストメソッドはパラメータを使って、 public void test(int testNumber) throws Throwable { switch(testNumber) { case 0:testfahrenheitToCelsius();break; case 1:testcelsiusToFahrenheit();break; case 2:testisHotter();break; case 3:testisCooler();break; case 4:testcelsiusToFahrenheit(20F, 68F);break; default: break; } } JMUnit TestSuiteテストスイートは1つ以上のテストケースを管理します。JMUnitには2つのテストスイート抽象クラス(jmunit.framework.cldc10.TestSuiteとjmunit.framework.cldc11.TestSuite)が用意されているので、これらを拡張してテストスイートを作成します。テストケースの場合と同様に、拡張すべきテストスイートのタイプは、どちらのバージョンのCLDCを使用するかで決まります。CLDC 1.0のアプリケーションにはcldc10.TestSuiteを使用し、CLDC 1.1のアプリケーションにはcldc11.TestSuiteを使用してください。どちらのテストスイート抽象クラスにも、パラメータとして文字列を受け取るコンストラクタがあります。この文字列はテストスイートの説明となります。 テストスイートの機能は、該当するすべてのテストケースのインスタンスを作成し、それらのテストケースのテストメソッドを呼び出すことだけです。テストスイートにテストケースを追加するには、テストスイートの構築時に import jmunit.framework.cldc11.TestSuite; public class ConversionTestSuite extends TestSuite { public ConversionTestSuite() { super("All Conversion Tests"); add(new DistanceConversionTest()); add(new TemperatureConversionTest()); } } JMUnitテストの実行JMUnitの抽象クラスであるTestCaseとTestSuiteは、MIDletのサブクラスです。これにより、個々のテストケースまたはテストスイートをエミュレータで(または、ありそうもないことですが、実際のデバイスで)実行することができます。シミュレータ上で実行すると、各テストケースまたはテストスイートは、exitとtestという2つのコマンドを提供します。図1は、上述のテストスイートの実行結果を示しています。図2は、失敗がどのように表示されるかを示しています。 図2 失敗したテストケース: テストケースが失敗すると、失敗が赤でグラフィカルに示される ![]() テストの実行時にはコンソールもチェックしたいものです(図3を参照)。とりわけ、失敗についてはコンソール出力を見るほうがよくわかります。失敗の出力には、スタックトレース情報と、テストでの実際の値と期待される値が含まれています。これは次に取り上げるJ2MEUnitと比べると、少し見劣りする機能の1つかもしれません。J2MEUnitでは、テストケースの失敗が、コンソール出力の代わりに、エミュレーションデバイス上に実際に表示されます。 J2MEUnitでの作業J2MEUnitのセットアップJMUnitの代替フレームワークがJ2MEUnitです。こちらもSourceForgeを通じてダウンロードできます。JMUnitと違って、J2MEUnitのバージョンは1つしかないので、.jarファイルも1つだけです。現在のリリースは1.1.1です。 J2MEUnitのテストケースJ2MEUnitのテストケースはj2meunit.framework.TestMethodから継承しなければなりません。JMUnitと同様、それぞれのテストをテストメソッドで実装し、「test」+「テスト対象メソッドの名前」という形式の名前を付けます。JMUnitと異なり、テストメソッドで例外をスローする必要はありません。 J2MEUnitはJMUnitほど多くのアサートメソッドをサポートしていません。J2MEUnitがサポートしているアサーションは次のものだけです。 assertTrue(expression) assertSame(expected, actual) assertEquals(expected, actual) assertNotEquals(expected, actual) assertNull(object) また、これらのメソッドはJMUnitのようにオーバーロードされません。例として、JMUnitの 表1
アサーションメソッドが少ないことを別にすれば、J2MEUnitのテストケースメソッドはJMUnitのテストケースメソッドとそれほど違いません。DistanceConverstionTestのテストメソッドの例をリスト1に示します。スローされる例外がないこと以外、メソッドの内容は同じです。 リスト1 DistanceConverstionTestのテストメソッドの例
public void testfeetToMeters() { System.out.println("feetToMeters"); int result = DistanceConversion.feetToMeters(25); assertEquals(7, result); } public void testmetersToFeet() { System.out.println("metersToFeet"); int result = DistanceConversion.metersToFeet(5); assertEquals(16, result); } public void testmilesToKM() { System.out.println("milesToKM"); com.white.DistanceConversion instance = null; int result = DistanceConversion.milesToKM(26); assertEquals(42, result); } public void testkmToMiles() { System.out.println("kmToMiles"); int result = DistanceConversion.kmToMiles(2); assertEquals(1, result); } public void testLongerThan() { System.out.println("longerThan"); assertTrue(500F < (DistanceConversion.kmToMiles(1) * 5280)); } JUnitおよびJMUnitと同様、J2MEUnitにもテストケースでオーバーライドできる J2MEUnit TestSuiteJ2MEUnitでテストスイートを作成および実行する方法は、JMUnitとかなり違っています。J2MEUnitのテストスイートの方が、通常のJUnitのテストスイートを連想させるものです(ただし、リフレクションはありません)。 J2MEUnitのテストスイートを作成するには、通常、テストケースクラス内に テストケースのためのテストメソッドをスイートに追加するのは、やや複雑そうに見えるかもしれません。スイートにテストメソッドを追加する最善の方法は、j2meunit.framework.TestMethodのインスタンスを作成して使用することです。TestMethodは実際にはインタフェースであり、実装クラスによって単一の public Test suite() { TestSuite suite=new TestSuite(); suite.addTest(new DistanceConverstionTest( "testfeetToMeters", new TestMethod() { public void run(TestCase tc) { ((DistanceConverstionTest) tc).testfeetToMeters(); } } )); return suite; } もちろん、DistanceConversionTestの他のメソッドも同様にスイートに追加する必要があります。 public DistanceConverstionTest(String testName, TestMethod testMethod) { super(testName, testMethod); } J2MEUnitテストの実行J2MEUnitのテストケースは、JMUnitと違ってMIDletのサブクラスではありません。ただし、J2MEUnitにはTestRunnerの2つの実装があり、これによってテストを実行することができます。j2meunit.textui.TestRunnerは、通常のJUnitのTestRunnerの考え方に対応するものです。j2meunit.textui.TestRunnerは、コマンドライン環境からのみ実行できます。したがって、もう一方のTestRunner実装を使用するほうが、J2MEUnitのテストを実行する方法として好ましいでしょう。j2meunit.midletui.TestRunnerは、あなたが拡張するMIDletです。MIDletとして、j2meunit.midletui.TestRunnerはエミュレータ(または実際のデバイス)で実行されます。 各自のTestRunner MIDletクラスで行うべきコーディングは、 public class ConversionTestRunnerMIDlet extends TestRunner { protected void startApp() { start(new String[] {"com.white.tests.j2meunit.DistanceConversionTest", "com.white.tests.j2meunit.TemperatureConversionTest"}); } } また、J2MEUnitでは、テストケースをMIDletに渡し、JADファイルまたはJARのマニフェストファイルで TestRunner MIDletは、JMUnitのような派手なグラフィックの結果を提供しませんが(図4を参照)、失敗に関しては(図5を参照)JMUnitと違ってエミュレータで情報を提供します。 図4 成功したテストスイートの実行: J2MEUnitによる出力はJMUnitほど派手ではないが、開発者にとって有用な情報を提供することに変わりはない。最初の画面には実行の状態が表示され、2番目の画面には最終結果が表示される JMUnitと同様、結果のコンソールウィンドウで、テストに関するさらに詳しい情報(失敗については特に)を見ることができます(図6を参照)。 NetBeansの統合JMUnitとJ2MEUnitは、ほとんどすべてのJava ME開発環境やSunのWireless Took KitなどのIDEで使用できますが、あるIDEで、これらのMicro Edition単体テストツールを合体したものが近いうちにリリースされます。NetBeans 5.5/NetBeans Mobility Pack 5.5に、これらのツールの両方が付属する予定です。実のところ、NetBeansはどんなJ2MEプロジェクトクラスのJMUnit CLDC 1.0テストでも自動的に生成してくれます。Mobileプロジェクトの任意のクラスを右クリックし、[Tools]メニューオプションからJUnitテストクラスの生成を選択するだけよいのです(図7を参照)。 図7 JMUnitのテストケースクラスを生成する: NetBeans Mobility Pack 5.5には、モバイルアプリケーションクラスのためのJMUnit CLDC 1.0のテストケースクラスを作成する単純なメニューオプションがある NetBeansは、デフォルトではJMUnit CLDC 1.0テストケースを生成します。プロジェクトのプロパティダイアログウィンドウを使用してJMUnit CLDC 1.1またはJ2MEUnitのライブラリをプロジェクトに追加し、それから手動で任意の型のテストクラスとMIDletテストケースランナーを手動で作成できます(図8を参照)。 図8 プロジェクトにライブラリを追加する: NetBeans Mobility Pack 5.5には、モバイルアプリケーションクラスのためのJMUnit CLDC 1.0のテストケースクラスを作成する単純なメニューオプションがある JMUnit CLDC 1.1またはJ2MEUnitのライブラリをNetBeans 5.5のモバイルプロジェクトに追加するには、プロジェクトを右クリックし、[Properties]を選択します。次に表示されるウィンドウで、図のように[Build]の下層にある[Libraries & Resources]を選択し、それから[Add Library]ボタンをクリックして、目的のプロジェクトで使用する他のJUnitライブラリを選択します。 NetBeansプロジェクトチームのメンバによると、NetBeans 5.5とNetBeans Mobility Pack 5.5は10月の終わり頃にリリースされる予定だそうです(執筆当時)。両方のQ-ビルドリリース(およびJava ME単体テストフレームワーク)がここから入手できます。 本稿のダウンロードサンプルには、本稿で紹介したすべてのクラスに加え、新しいNetBeans IDEにすばやく簡単に導入できるNetBeans 5.5プロジェクトも含まれています。「Conversion 1.0」は、CLDC 1.0バージョンのコード例と、JMUnit CLDC 1.0およびJ2MEUnitのテストケースが含まれているプロジェクトです。「Conversion 1.1」は、CLDC 1.1バージョンのコード例と、JMUnit CLDC 1.1のテストケースが含まれているプロジェクトです。 Java ME単体テストの将来は明るい自分のJava Micro Editionアプリケーションの品質が悪いのは適切な単体テストツールがないせいだ、という言い訳はもう通用しません。JMUnitとJ2MEUnit(そして予定されている両者の合体版)は、Micro Edition開発者に有益な品質保証支援を提供します。 この2つのフレームワークはいくつかの点で異なっています。両者の相違点の多くは、大部分の開発者が使用するAPIの実装の詳細部分にあります。JMUnitの方がアサーション群が豊富で、実装がやや簡単かもしれません。それに対し、J2MEUnitはスイートレベルでの見た目と動作が通常のJUnitに似ていると言えるでしょう。どちらも素晴らしい機能を備えているので、そうした機能は合体した製品でも存続してほしいものです。 著者紹介Jim White(Jim White)
|