目次


Eclipse 流のユニット・テスト

Eclipse IDE での Java 用テスト・フレームワーク RMock で jMock を機能強化する

Comments

モック・オブジェクトは、コードの実行範囲をテスト対象領域内に収めることのみを目的に作成されたクラスの動作を偽装します。しかし時間が経つにつれ、アプリケーション・クラスの数が増えると、モック・オブジェクトの数も増加していきます。jMock や RMock、さらには EasyMock といったフレームワークは、物理的に別途存在する一連のモック・オブジェクトを必要ないようにするために役立ちます。

EasyMock フレームワークの大きな欠点の 1 つは、具象クラスを偽装することができず、インターフェースしか偽装できないことです。この記事では、jMock フレームワークを使って、具象クラスそしてインターフェースを偽装する方法と、RMock を使って困難なケースをテストする方法について説明します。

Eclipse IDE で jMock と RMock を構成する

注意: JUnit と jMock、RMock の最新のバイナリーについては「参考文献」を参照してください。

まず Eclipse IDE (integrated development environment: 統合開発環境) を起動します。次に、基本となる Java™ プロジェクトを作成し、TestingExample という名前を付けます。このプロジェクトには、JUnit と jMock、RMock の JAR (Java Archive) のライブラリーをインポートします。Java Perspective で Project > Properties を選択し、次に Libraries タグをクリックします。これを下記に示します。

図 1. Eclipse で TestingExample プロジェクトのプロパティーを編集する
Editing properties for the TestingExample project in Eclipse
Editing properties for the TestingExample project in Eclipse

Add JARs ボタンは JAR ファイルを Java クラスパス (つまり Eclipse の中で構成したJRE (Java Runtime Environment) です) に入れる場合に使います。Add Variable ボタンは、(ローカル、あるいはリモートの) ファイルシステム上の (JAR を含む) リソースが存在している、通常参照される特定のディレクトリーに対して使用します。Add Library ボタンは、Eclipse でデフォルトのリソース、あるいは特定の Eclipse ワークスペース環境用に構成されたリソースを参照する必要がある場合に使います。Add Class Folder をクリックするのは、プロジェクトの一部として既に構成されている既存のプロジェクト・フォルダーの 1 つからリソースを追加する場合です。

この例では、Add External JARs をクリックし、ダウンロードした jMock と RMock の JAR までブラウズします。これらの JAR をプロジェクトに追加します。図 2 に示すプロパティー・ウィンドウが表示されたら OK をクリックします。

図 2. TestingExample プロジェクトに追加された jMock と RMock の JAR
Listing of jMock and RMock JARs added to the TestingExample Project
Listing of jMock and RMock JARs added to the TestingExample Project

TestExample のソース・コード

TestExample プロジェクトの場合は、次の 4 つのクラスのソース・コードを扱います。

  • ServiceClass.java
  • Collaborator.java
  • ICollaborator.java
  • ServiceClassTest.java

テスト対象のクラスは ServiceClass で、このクラスは runService() というメソッドを 1 つ含んでいます。このサービス・メソッドには Collaborator という名前のオブジェクトがあり、Collaborator は単純なインターフェース、ICollaborator を実装します。具象 Collaborator クラスには、executeJob() という 1 つのメソッドが実装されます。ここでは、Collaborator クラスを適切に偽装する必要があります。

4 番目のクラスは ServiceClassTest というテスト・クラスです。(この実装は可能な限り単純化されています。) リスト 1 は、この 4 番目のクラスのコードを示しています。

リスト 1. サービス・クラスのサンプル・コード
public class ServiceClass {
public ServiceClass(){
//no-args constructor
}

public boolean runService(ICollaborator collaborator){
if("success".equals(collaborator.executeJob())){
return true;
}
else
{
return false;
}
}
}

ServiceClass クラスの中で、if...else コード・ブロックは単純な論理分岐です。この分岐があることで、テストの想定に従って (他のパスではなく) そのパスを使った場合に、なぜテストに失敗、あるいはパスするのかを示すことができます。Collaborator クラスのソース・コードを下記に示します。

リスト 2. Collaborator クラスのサンプル・コード
public class Collaborator implements ICollaborator{
public Collaborator(){
//no-args constructor
}
public String executeJob(){
return "success";
}
}

Collaborator クラスも単純であり、引数なしのコンストラクターと、メソッド executeJob() から返される単純な String を持っています。下記のコードは ICollaboratorクラスのコードを示しています。

public interface ICollaborator {
public abstract String executeJob();
}

インターフェース ICollaborator には、Collaborator クラスで実装する必要があるメソッドが 1 つ含まれます。

上記のコードが用意できたので、今度は、さまざまなシナリオで ServiceClass クラスのテストを正常に実行する方法の検証に移りましょう。

シナリオ 1: jMock を使ってインターフェースを偽装する

ServiceClass クラスのサービス・メソッドをテストするのは簡単です。例えばテストの要件が、runService() メソッドが実行されなかったことをアサートすること、つまり返された結果のブール値が偽であることをアサートすることだとしましょう。こうした場合、runService() メソッドに渡される ICollaborator オブジェクトを偽装して ICollaborator のメソッド executeJob() の呼び出しを想定し、そして “success” 以外のストリングを返します。こうすることで、テストに対して確実にブール・ストリングの偽が返されます。

以下に示す ServiceClassTest クラスのコードは、このテスト・ロジックを含んでいます。

リスト 3. シナリオ 1 に対するServiceClassTest クラスのサンプル・コード
import org.jmock.Mock;
import org.jmock.cglib.MockObjectTestCase;
public class ServiceClassTest extends MockObjectTestCase {
private ServiceClass serviceClass;
private Mock mockCollaborator;
private ICollaborator collaborator;

public void setUp(){
serviceClass = new ServiceClass();
mockCollaborator = new Mock(ICollaborator.class);
}

public void testRunServiceAndReturnFalse(){
mockCollaborator.expects(once()).method\
("executeJob").will(returnValue("failure"));
collaborator = (ICollaborator)mockCollaborator.proxy();
boolean result = serviceClass.runService(collaborator);
assertFalse(result);
}
}

一般的に、さまざまなテスト・ケースにわたって共通の操作を行う場合には、テストの中に setUp() メソッドを含めることが賢明です。tearDown() メソッドを含めるのも良い考えですが、結合テストを実行するのでない限り、必ずしも必要ではありません。

また、jMock と RMock では、テストを実行した後、あるいはテスト実行中に、すべてのモック・オブジェクトに対するすべての想定事項をフレームワークがチェックすることにも注意してください。各モック・オブジェクトの想定事項に対して、実際に verify() メソッドを含める必要はありません。上記テストを JUnit テストとして実行すると、以下のようにパスします。

図 3. シナリオ 1 のテストにパスする
Scenario 1 test pass
Scenario 1 test pass

ServiceTestClass クラスは jMock CGLIB の org.jmock.cglib.MockObjectTestCase クラスを継承します。mockCollaborator は単純なorg.jmock.JMock クラスです。通常、jMock を使ってモック・オブジェクトを作成する方法としては、次の 2 つがあります。

  • インターフェースを偽装するためには new Mock(Class.class) メソッドを使います。
  • 具象クラスを偽装するためには mock(Class.class, "identifier") メソッドを使います。

重要な点として、モック・プロキシーがどのように ServiceClass クラスの runService() メソッドに渡されるかに注意してください。jMock では、想定事項が既に設定されて作成されたモック・オブジェクトから、プロキシー実装を抽出することができます。この点は、この記事の後の方で紹介するシナリオ、特に RMock が関係する場合に非常に重要になってきます。

シナリオ 2: jMock を使ってデフォルトのコンストラクターを持つ具象クラスを偽装する

ここで、ServiceClass クラスの runService() メソッドが Collaborator クラスの具象実装しか受け付けなかった、としましょう。jMock を使えば、想定事項を変更しなくても確実に先ほどのテストにパスするのでしょうか。もし Collaborator クラスを単純なデフォルトの方法で構成できるのであれば、答えはイエスです。

ServiceClass クラスの runService() メソッドを変更し、以下のコードにします。

リスト 4. シナリオ 2 のために変更された ServiceClass クラス
public class ServiceClass {
public ServiceClass(){
//no-args constructor
}

public boolean runService(Collaborator collaborator){
if("success".equals(collaborator.executeJob())){
return true;
}
else{
return false;
}
}
}

ServiceClass クラスの if...else ロジック分岐は (わかりやすくするため) 同じままです。また、引数なしのコンストラクターも、そのままです。while...do 文や for ループといった、工夫が必要なロジックは、クラスのメソッドを適切にテストするためには必ずしも必要ではないことに注意してください。そのクラスが使用するオブジェクトに対して実行するメソッドがある限り、そうしたメソッドをテストするには、想定事項を単純に偽装したたもので十分です。

このシナリオに合うように、ServiceClassTest クラスも以下のように変更する必要があります。

リスト 5. シナリオ 2 のために変更された ServiceClassTest クラス
...
private ServiceClass serviceClass;
private Mock mockCollaborator;
private Collaborator collaborator;

public void setUp(){
serviceClass = new ServiceClass();
mockCollaborator = mock(Collaborator.class, "mockCollaborator");
}

public void testRunServiceAndReturnFalse(){
mockCollaborator.expects(once()).method("executeJob").will(returnValue("failure"));
collaborator = (Collaborator)mockCollaborator.proxy();
boolean result = serviceClass.runService(collaborator);
assertFalse(result);
}
}

ここで、いくつか注意すべき点があります。第 1 に、runService() メソッドのシグニチャーは先ほどのものから変更されています。このメソッドは、今度は ICollaborator インターフェースを受け付けるのではなく、具象クラス実装 (Collaborator クラス) を受け付けるようになっています。この変更は、テストのフレームワークに関する限りは重要です (具象クラスを渡す例は、本来ポリモーフィズムに反することですが、ここではあくまでも例を示すことを目的として使われていることに注意してください。真のオブジェクト指向では、決してこんなことをすべきではありません。)

第 2 に、Collaborator クラスを偽装する方法が変更されています。jMock の CGLIB ライブラリーを使うと、具象クラスの実装を偽装することができます。jMock CGLIB の mock() メソッドに与えられる String パラメーターは、作成されるモック・オブジェクトの識別子として使われています。jMock を使う場合には (そしてもちろんRMock を使う場合にも)、1 つのテスト・ケース内に設定されたモック・オブジェクトごとに固有の識別子が必要です。これは、共通の setUp() メソッドの中で定義されるモック・オブジェクトにも、実際のテスト・メソッドの中で定義されるモック・オブジェクトに当てはまります。

第 3 に、テスト・メソッドに対する元々の想定事項は変更されていません。このテストにパスするためには、相変わらず偽のアサーションが必要です。これは重要です。というのは、使われているテスト・フレームワークの柔軟性を、異なる入力による変更にも対応して一定のテスト結果を得られることで示したとしても、ある入力には対応できず同じ結果が得られない場合には、そのフレームワークの本当の限界を示すことになるからです。

では、このテストを JUnit テストとして再度実行しましょう。以下のように、このテストにはパスします。

図 4. シナリオ 2 のテストにパスする
Scenario 2 test pass
Scenario 2 test pass

次のシナリオでは、もう少し複雑になります。今度は RMock フレームワークを使うことで、困難と思える状況を比較的容易に緩和することができます。

シナリオ 3: jMock と RMock を使って、デフォルトではないコンストラクターを持つ具象クラスを偽装する

まず、先ほどと同じように、Collaborator オブジェクトを偽装するために、まず jMock を使ってみましょう。今回だけは、Collaborator はデフォルトの引数なしのコンストラクターを持ちません。このテストでも、ブール値の偽が想定されていることは同じであることに注意してください。

また、Collaborator オブジェクトが、コンストラクターに渡されるパラメーターとしてストリングと基本型の int を要求するとしましょう。リスト 6 は、Collaborator オブジェクトに加えられた変更を示しています。

リスト 6. シナリオ 3 のために変更された Collaborator クラス
public class Collaborator{
private String collaboratorString;
private int collaboratorInt;

public Collaborator(String string, int number){
collaboratorString = string;
collaboratorInt = number;
}
public String executeJob(){
return "success";
}
}

Collaborator クラスのコンストラクターは、まだ非常に単純です。クラスのフィールドは入力パラメーターによって設定されます。ここでは他のロジックは何も必要なく、このクラスの executeJob() 関数は単純なままです。

例の中の他のコンポーネントはすべて同じままとして、テストを再度実行します。そうすると結果は、破滅的なテスト失敗となります (下図)。

図 5. シナリオ 3 のテストに失敗する
Scenario 3 test failure
Scenario 3 test failure

上記のテストは、コード・カバレッジを使わずに単純な JUnit テストとして実行されたものです。この記事で取り上げているどのテストも、たいていのコード・カバレッジ・ツールを使って実行することができます (例えば Cobertura や EclEmma など)。しかし、Eclipse の中でコード・カバレッジを使って RMock テストを実行する場合には、いくつかの問題があります (表 1 を見てください)。下記のコードは、実際のスタック・トレースのスニペットを示しています。

リスト 7. シナリオ 3 のテスト失敗に対するスタック・トレース
...Superclass has no null constructors but no arguments were given
at net.sf.cglib.proxy.Enhancer.emitConstructors(Enhancer.java:718)
at net.sf.cglib.proxy.Enhancer.generateClass(Enhancer.java:499)
at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:660)
.....
.....

この失敗の理由は、jMock が、引数なしのコンストラクターを持たないクラス定義から実行可能なモック・オブジェクトを作成できないことにあります。Collaborator オブジェクトをインスタンス化するための唯一の方法は、2 つの単純な引数を提供することです。つまり、同じ結果が得られるようにモック・オブジェクトのインスタンス化プロセスに引数を提供する方法を、なんとかして見つけなければなりません。RMock を使う理由は、正にここにあります。

失敗したテストを RMock テスト・フレームワークで修正する

テストを修正するために、いくつかの変更を行う必要があります。これらは大きな変更に思えるかもしれませんが、突き詰めると、こうした変更は、両方のフレームワークの機能を活用して目的を達成するための比較的簡単な回避策なのです。

最初に必要な変更は、テスト・クラスを、jMock の CGLIB TestCase ではなく RMock の TestCase にすることです。目標は、こうした、RMock に属するモック・オブジェクトを、テスト自体の中で、そして (もっと重要なこととして) テストの初期設定の際に、容易に構成できるようにすることです。これまでの経験から、テスト・クラスを継承する元となる TestCase オブジェクト全体が RMock に属する場合には、両方のフレームワークでモック・オブジェクトを作成し、使用した方が簡単なことがわかっています。さらに、一目見れば、モック・オブジェクトのフローを素早く判断する方が多少簡単なことがわかります (ここで言うフローは、モック・オブジェクトをパラメーターとして (さらには他のモック・オブジェクトからの戻り型として) 使用している状況を記述するために使われています)。

必要な変更の 2 番目は (最低限として)、Collaborator クラスのコンストラクターの中に渡されるパラメーターの実際の値を保持する、オブジェクト配列を作成することです。また、わかりやすくするために、コンストラクターが受け付ける型のクラス型配列を含めることもでき、そしてその配列と、モック Collaborator オブジェクトをインスタンス化するために先ほどパラメーターとして記述されたオブジェクト配列を渡すこともできます。

3 番目の変更として、RMock モック・オブジェクトに対する 1 つ以上の想定事項を、適切な構文で作成する必要があります。そして必要な最後の変更となる 4 番目の変更は、RMock モック・オブジェクトを記録状態から準備状態にすることです。

RMock の変更を実装する

リスト 9 は、ServiceClassTest クラスに対する最終的な変更を示しています。これは、RMock とその関連機能の導入も示しています。

リスト 9. シナリオ 3 用に ServiceClassTest クラスを修正する
...
import com.agical.rmock.extension.junit.RMockTestCase;
public class ServiceClassTest extends RMockTestCase {

private ServiceClass serviceClass;
private Collaborator collaborator;

public void setUp(){
serviceClass = new ServiceClass();
Object[] objectArray = new Object[]{"exampleString", 5};
collaborator =(Collaborator)intercept(Collaborator.class, objectArray, "mockCollaborator");
}

public void testRunServiceAndReturnFalse(){
collaborator.executeJob();
modify().returnValue("failure");
startVerification();
boolean result = serviceClass.runService(collaborator);
assertFalse(result);
}
}

まず、テストの想定事項が相変わらず変更されていないことに注目してください。RMockTestCase クラスをインポートすることで、RMock フレームワークの機能が導入されたことがわかります。次に、このテスト・クラスは、今度は MockObjectTestCase ではなく RMockTestCase を継承します。後ほど、TestClass オブジェクトが RMockTestCase オブジェクト型のままであるテスト・ケースに MockObjectTestCase を再導入するところを示します。

setUp() メソッドの中で、Collaborator クラスのコンストラクターが必要とする実際の値でオブジェクトの配列をインスタンス化します。この配列は、まとめて RMockintercept() メソッドに与えられ、モック・オブジェクトのインスタンス化の際に役立ちます。このメソッドのシグニチャーは、jMock の CGLIB mock() メソッドのシグニチャーに似ていますが、それはどちらのメソッドも固有のモック・オブジェクト識別子を引数として持つからです。intercept() メソッドは Object 型を返すため、モック・オブジェクトを Collaborator 型にクラス・キャストする必要があります。

テスト・メソッドそのものである testRunServiceAndReturnFalse() の中に、さらにいくつかの変更があることがわかります。モック Collaborator オブジェクトの executeJob() メソッドが呼び出されています。この時点では、このモック・オブジェクトは記録状態にあります。つまり、このモック・オブジェクトがこの先想定するメソッド呼び出しが単純に定義されています。そのためこのモック・オブジェクトは、その定義に従って想定事項を記録しています。次の行は、モック・オブジェクトに対して、executeJob() メソッドに突き当たったら必ず failure というストリングを返さなければならないことを通知しています。従って RMock では、モック・オブジェクトのメソッドを単純に呼び出すことで (そしてモック・オブジェクトが必要とする可能性のあるパラメーターを渡すことで) 想定事項を宣言します。そしてその想定事項を変更し、それに従って戻り型を調整します。

最後に、RMock のメソッド startVerification() が呼び出され、モック Collaborator オブジェクトを準備状態にしています。これでこのモック・オブジェクトを、実在するオブジェクトとして ServiceClass クラスの中で使うことができます。このメソッドは非常に重要であり、テストの初期化に失敗しないためには、必ずこのメソッドを呼び出す必要があります。

変更をテストする

ServiceClassTest を再度実行すると、最終的にテストに成功します。モック・オブジェクトをインスタンス化する際に提供したパラメーターによって、すべてが変わったのです。図 6 は、JUnit で成功が緑色で表示されている様子です。

図 6. RMock でシナリオ 3 のテストが成功する
Scenario 3 test success with RMock
Scenario 3 test success with RMock

assertFalse(result) というコード行はシナリオ 1 と同じテストの想定事項を表現しており、先ほどの jMock の場合と同じように、RMock でもテストは成功します。これはさまざまな意味で重要ですが、ここでもっと注目すべきことは、テストの想定事項を変更することなく、壊れたテストの修正というアジャイルの原則が適用された点かもしれません。唯一の違いは、異なるフレームワークが使われたという点のみです。

次のシナリオでは、ある特別なケースとして jMock と RMock の両方を使います。どちらのフレームワークも、一方のみでは適切な結果を得られず、テストの中で両者を何らかの方法で連携させる必要があります。

シナリオ 4: jMock と RMock の間での特別な連携

先ほど触れたように、私は、ある種の結果を達成するために 2 つのフレームワークが連携して動作しなければならないケースを検証したかったのです。そうでないと、適切に作成されたテストが毎回失敗してしまいます。jMock を使っても RMock を使っても無関係であるというケースはほとんどありません。そういったケースとしては、例えば偽装対象のインターフェースあるいはクラスが、署名された JAR の中に存在する場合などがあります。そうした状況は稀ですが、セキュアな独自製品 (通常は、なんらかの既製ソフトウェア) の API (application program interface) 用に作成されたコードをテストする際に発生する可能性があります。

リスト 10 は、両方のフレームワークがテスト・ケースを処理する例を示しています。

リスト 10. シナリオ 4 に対するテストの例
public class MyNewClassTest extends RMockTestCase{

private MyNewClass myClass;
private MockObjectTestCase testCase;
private Collaborator collaborator;
private Mock mockClassB;

public void setUp(){
myClass = new MyNewClass();

testCase = new MyMockObjectTestCase();

mockClassB = testCase.mock(ClassB.class, "mockClassB");
mockClassB.expects(testCase.once()).method("wierdMethod").will(testCase.returnValue("passed"));

Class[] someClassArray = new Class[]{String.class, ClassA.class, ClassB.class};
Object[] someObjectArray = new Object[]
{"someArbitraryString", new ClassA(), (ClassB)mockClassB.proxy()};

collaborator = (Collaborator)intercept(Collaborator.class, someClassArray, someObjectArray, "mockCollaborator");
}

public void testRMockAndJMockInCollaboration(){
startVerification();
assertTrue(myClass.executeJob(collaborator));
}

private class MyMockObjectTestCase extends MockObjectTestCase{}

private class MyNewClass{
public boolean executeJob(Collaborator collaborator){
collaborator.executeSomeImportantFunction();
return true;
}
}
}

setUp() メソッドの中で、jMock-CGLIBMockObjectTestCase オブジェクトを継承するために作成されたプライベート内部クラスに基づいて、新しい "testcase" がインスタンス化されています。このちょっとした次善の策は、テスト・クラス全体を RMock の TestCase オブジェクトとして保つ一方で任意の jMock 機能を扱うために必要です。例えば、jMock の想定事項は、once() ではなく testCase.once() のように設定されることになります。それは、TestClass オブジェクトが RMockTestCase を継承するからです。

ここでは、ClassB クラスに基づくモック・オブジェクトが作成され、このモック・オブジェクトに想定事項が提供されています。次にこのオブジェクトを使って RMock の Collaborator モック・オブジェクトをインスタンス化します。テスト対象のクラスは MyNewClass クラスです (ここではプライベート内部クラスとして示してあります)。この場合も、このクラスの executeJob() メソッドは Collaborator オブジェクトを受け取り、そして executeSomeImportantFunction() メソッドを実行します。

リスト 11 と 12 は、それぞれ ClassAClassB のコードを示しています。ClassA は単純なクラスで実装を持ちませんが、ClassB はポイントを説明するための最低限の詳細を示しています。

リスト 11. ClassA クラス
public class ClassA{}

このクラスは単なるダミー・クラスであり、(コンストラクターがオブジェクト・パラメーターを受け取る) クラスを偽装するために RMock が必要である、というポイントを強調するために使用されています。

リスト 12. ClassB クラス
public class ClassB{
public ClassB(){}
public String wierdMethod(){
return "failed";
}
}

ClassB クラスの wierdMethodfailed を返します。これは重要です。なぜなら、テストにパスさせるためには、このクラスはすぐ後に別のストリングを返す必要があるからです。

リスト 13 は、このテストの例の最も重要な部分、Collaborator クラスを示しています。

リスト 13. Collaborator クラス
public class Collaborator {
private String  _string;
private ClassA _classA;
private ClassB _classB;

public Collaborator(String string, ClassA classA, ClassB classB) throws Exception{
_string = string;
_classA = classA;
if(classB.wierdMethod().equals("passed")){
_classB =classB;
}
else{throw new Exception("Something bad happened");
}
}

public void executeSomeImportantFunction(){
}
}

ここで何よりも重要な点として、jMock フレームワークを使って ClassB クラスを偽装したことに注目してください。RMock では、モック・オブジェクトからプロキシーを抽出して使用する方法がないため、テスト setUp() メソッドの他の場所でプロキシーを使うことができません。また RMock では、プロキシー・オブジェクトは startVerification() メソッドが呼び出された後でしか現れません。こういった場合に jMock が優れている点は、他のモック・オブジェクトを設定する際に、もしそれらのオブジェクトが (それ自体がモック・オブジェクトである) オブジェクトを返さなければならない場合であっても、設定に必要なものを入手できるからです。

2 番目に気付く点として、逆に jMock フレームワークを使って Collaborator クラスを偽装することはできないということです。その理由は、このクラスが引数なしのコンストラクターを持っていないためです。しかもこのクラスのコンストラクターの中には、最初にインスタンスを取得できるかどうかを判断する、ある種のロジックがあります。実際、この点について言えば、ClassBwierdMethod() メソッドは、Collaborator オブジェクトがインスタンス化されるためには passed を返す必要があります。しかし、このメソッドが、デフォルトで必ず failed を返すことにも注目してください。つまりテストを成功させるためには、明らかに ClassB を偽装する必要があるのです。

また、これまでの例とは異なり、このシナリオでのクラス配列は intercept() メソッドに対する追加パラメーターとして含まれています。このクラス配列は厳密には必要ではありませんが、どのオブジェクト・クラスを RMock テスト・オブジェクトのインスタンス化に使用したかを素早く識別するための鍵となります。

この新しいテスト・ケースを実行すると、今度は成功します。図 7 はめでたく成功したところを示しています。

図 7. RMock と jMock の連携によりシナリオ 4 のテストが成功する
Scenario 4 test success with RMock and jMock in collaboration
Scenario 4 test success with RMock and jMock in collaboration

Collaborator モック・オブジェクトが適切に設定されており、また mockClassB オブジェクトが期待した通りの動作をしています。

テスト・ツールの違いを簡単に調べる

さまざまなシナリオで見てきたように、jMock と RMock は、どちらも Java コードをテストするための強力なツールです。しかし、開発やテストに使用される他のツールと同じように、必ず制約があります。もちろん、他にも利用できるテスト・ツールはありますが、(Java 技術では) どれも RMock と jMock ほどうまく動作しません。私の個人的な経験では、Microsoft® .NET フレームワークにはいくつかの強力なツールもあります (例えば TypeMock など)。しかし、それらはこの記事の対象外であり、それにプラットフォームも異なります。

表 1 は、Rmock と jMock のフレームワーク間の違いと、これまでに経験された起こりうる問題、特に Eclipse 環境での問題を示したものです。

表 1. RMock と jMock の両テスト・フレームワーク間の違い
テスト用のモック・スタイルjMock RMock
インターフェースの偽装が可能か可能: new Mock() メソッド可能: mock() メソッド
具象クラスの偽装が可能か可能: CGLIB を持つ mock() メソッド可能: mock() メソッドあるいは intercept() メソッド
任意の具象クラスの偽装が可能か不可: 引数なしのコンストラクターが存在する必要がある可能
いつでもプロキシーを取得できるか可能不可: startVerification() が準備状態になった後のみ
他の Eclipse プラグインとの間の問題既知の問題はないあり: Eclipse 用の CoverClipse プラグインでのメモリー内競合

まとめ

これらのフレームワークは、ユニット・テストの結果の生成に強力な力を発揮するので、皆さんも使ってみることをお勧めします。多くの Java 開発者は、頻繁にテストを作成することに慣れていません。実際にテストを作成する場合であっても、たいていは、作成されるテストはあるメソッドの中心機能をカバーする非常に単純なものになりがちです。コードの中の、「テストしにくい」部分をテストするためには、jMock と RMock は非常に優れた選択肢です。

jMock と RMock を使うことで、コード内にあるバグを劇的に削減でき、また実証された方法を使ってプログラム・ロジックをテストするスキルを向上させることができます。また、改善されたこれらのフレームワークや他のフレームワークに関してドキュメンテーションを読み、実験することは、皆さんの開発スキルの向上にも役立つはずです (そしてこれまで許していた、貧弱に作成されたコードを許さなくなるはずです)。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, Java technology
ArticleID=236683
ArticleTitle=Eclipse 流のユニット・テスト
publish-date=05292007