本文へジャンプ

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む


お客様が developerWorks に初めてサインインすると、プロフィールが作成されます。プロフィールで選択した情報は公開されますが、いつでもその情報を編集できます。お客様の姓名(非表示設定にしていない限り)とディスプレイ・ネームは、投稿するコンテンツと一緒に表示されます。

送信されたすべての情報は安全です。

  • 閉じる [x]

developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む


送信されたすべての情報は安全です。

  • 閉じる [x]

AspectJおよび疑似オブジェクトによる柔軟なテスト

テスト専用の振る舞いによる単体テストの拡張

Nicholas Lesiecki (ndlesiecki@apache.org), Technical Team Lead, eBlox, Inc.
Nicholas Lesiecki氏は、ドットコム・ブームの時期にJavaプログラミングの世界に入り、それ以来、XPおよびJavaコミュニティーで頭角を現してきました。XPのような機敏なプロセスでオープン・ソース構築を活用し、ツールをテストするためのマニュアル 「Java Tools for Extreme Programming」 を手掛けました。 Tucson JUG に頻繁に登場するほか、JakartaのCactus プロジェクト (サーバー側単体テスト・フレームワーク) にも力を注いでいます。Nickの連絡先はndlesiecki@apache.org です。

概要: 自分の開発プロセスに単体テストを組み込んだことのあるプログラマーであれば、その利点を知っておられるでしょう。コードがより明瞭になり、リファクタリングが促進され、開発スピードがより速くなります。しかし、振る舞いがシステムの状態に依存しているクラスをテストするとなると、どんなに粘り強い単体テスト担当者でも行き詰まってしまうことがあります。この記事では、高い評価を受けているJavaプログラマーであり、XPコミュニティーのリーダーでもあるNicholas Lesiecki氏が、テスト・ケースの分離に関係する問題を紹介し、疑似(mock)オブジェクトとAspectJを使って正確で強力な単体テストを推し進めていく方法を示します。

日付:  2002年 5月 01日
レベル:  中級 この記事の原文:  英語
アクティビティー: 1797 ビュー
お気軽にご意見・ご感想をお寄せください: 


最近のエクストリーム・プログラミング (XP) への関心の高まりは、最も採用しやすいプラクティス(実践術)に向けられています。それは、単体テストとテストファースト型の設計です。ソフトウェア・メーカーがXPのプラクティスを採用するにつれて、多くの開発者は、広範囲にわたる単体テストの道具立てを手にすることにより、品質と開発スピードが向上するのを目にしてきました。しかし、優れた単体テストを記述するには、時間と労力が掛かります。それぞれのプログラム単位は相互に関連し合っているため、単体テストを記述することには、かなりの量のセットアップ(準備)・コードが必要となることがあります。このことがテストの費用を押し上げ、場合によっては (リモート・システムに対するクライアントとして動作するコードの場合など)、そのようなテストを実装するのがほとんど不可能なこともあります。

XPでは、単体テストは、統合テストと受け入れテストを補うものです。後者の2種類のテストは、別個のチームが実施したり、別個の作業として実施したりすることができます。しかし、単体テストは、テストするコードと同時に記述します。差し迫った締め切りに追われ、単体テストが頭痛の種になると、場当たり的にいいかげんなテストを記述する誘惑にかられたり、テストにまったく煩わされずにいたいと思ったりすることがあります。XPは積極的な動機と自立的なプラクティスに依存しているため、テストに焦点を合わせ、テストを記述しやすくすることは、XPプロセスにとって (そしてプロジェクトにとって) 最も注目すべきことです。

必要な予備知識

この記事は、AspectJによる単体テストに焦点を合わせているため、読者が単体テストの基本的な技法に習熟していることを前提としています。AspectJに習熟しておられない読者は、この記事を読み進める前に、AspectJに関する私の紹介記事 (参考文献 を参照) を読むことをお奨めします。この記事で紹介するAspectJの技法は特に複雑ではありませんが、アスペクト指向プログラミングには多少の慣れが必要です。実例を実行するためには、テスト・マシンにAnt をインストールしておく必要があります。しかし、実例を試してみるために、Antについて特別な専門知識 (基本的なインストールに必要な以上の知識) は不要です。Antの詳細とダウンロードについては、「参考文献」セクションを参照してください。

このジレンマを解決するには、疑似 (mock) オブジェクトが役立ちます。疑似オブジェクト・テストでは、ドメインへの依存関係を、テストのためだけに使用する疑似実装で置き換えます。しかし、この方策でさえも、リモート・システム上での単体テストなど、特定の状況では技術上の課題を抱えています。AspectJ (Java言語に対するアスペクト指向の拡張機能) は、従来のオブジェクト指向の技法ではうまくいかない分野で、テスト目的の振る舞いを置換できるようにすることにより、単体テストを成功へと導いてくれます。

この記事では、単体テストを記述することが望ましいものの、それが難しいという、一般的な状況を調べていきます。まず手始めに、EJBベースのアプリケーションのクライアント・コンポーネントについて単体テストを実行します。次に、その例を足掛かりにして、リモート・クライアント・オブジェクトの単体テストで発生することのあるいくつかの問題について説明します。それらの問題を解決するために、AspectJと疑似オブジェクトを利用した新しいテスト構成を開発します。この記事を読み終える頃には、AspectJと疑似オブジェクトによるテストが提供する興味深い可能性に開眼するとともに、単体テストに関する一般的な問題とその解決策についても理解されているに違いありません。

この記事全体で取り扱うコード例の理解を助けるために、ここで、サンプル・アプリケーションをインストールすることができます。

単体テストの例

この例は、EJBクライアントのテストで構成されています。このケース・スタディーで発生する問題の多くは、WebサービスやJDBCを呼び出すコード、またはファサード経由でローカル・アプリケーションの「リモート」部分を呼び出すコードにさえ当てはまります。

サーバー・サイドのCustomerManager EJBは、2つの機能を実行します。つまり、顧客の名前を検索し、新しい顧客名をリモート・システムに登録します。リスト1は、CustomerManager がクライアントに公開しているインターフェースを示しています。



リスト1. CustomerManagerのリモート・インターフェース
                
public interface CustomerManager extends EJBObject {

     /**
      * Returns a String[] representing the names of customers in the system
      * over a certain age.
      */
     public String[] getCustomersOver(int ageInYears) throws RemoteException;

     /**
      * Registers a new customer with the system. If the customer already 
      * exists within the system, this method throws a NameExistsException.
      */
     public void register(String name) 
       throws RemoteException, NameExistsException;
}

クライアント・コードはClientBean という名前で、基本的に同じメソッドを公開しており、その実装はCustomerManager に委譲しています (リスト2を参照)。



リスト2. EJBクライアント・コード
                
public class ClientBean {
       private Context initialContext;
       private CustomerManager manager;

       /**
        * Includes standard code for referencing an EJB.
        */
       public ClientBean() throws Exception{
           initialContext = new InitialContext();
           Object obj =
                  initialContext.lookup("java:comp/env/ejb/CustomerManager");
           CustomerManagerHome managerHome = (CustomerManagerHome)obj;

           /*Resin uses Burlap instead of RMI-IIOP as its default
            *  network protocol so the usual RMI cast is omitted.
            *  Mock Objects survive the cast just fine.
            */
           manager = managerHome.create();
       }

       public String[] getCustomers(int ageInYears) throws Exception{
           return manager.getCustomersOver(ageInYears);
       }

       public boolean register(String name) {
           try{
               manager.register(name);
               return true;
           }
           catch(Exception e){
               return false;
           }
       }
}

テストに注意を集中できるようにするため、このコード・ユニットは意図的に単純なものにしました。ClientBean のインターフェースは、CustomerManager のインターフェースと少しだけ違います。ClientManager とは違って、ClientBeanregister() メソッドは、顧客が既に存在する場合に例外をスローするのではなく、ブール値を返します。これらは、優れた単体テストであれば検査するべき関数です。

リスト3のコードは、JUnitによるClientBean の単体テストを実装しています。テスト・メソッドは3つあります。1つはgetCustomers() 用で、あと2つはregister() 用です (1つは成功、もう1つは失敗)。このテストは、getCustomers() が55項目のリストを返し、register() は、EXISTING_CUSTOMER に対してはfalseNEW_CUSTOMER に対してはtrue を返すことを想定しています。



リスト3. ClientBeanの単体テスト
                
//[...standard JUnit methods omitted...]

public static final String NEW_CUSTOMER = "Bob Smith";
public static final String EXISTING_CUSTOMER = "Philomela Deville";
public static final int MAGIC_AGE = 35;

public void testGetCustomers() throws Exception {
       ClientBean client = new ClientBean();
       String[] results = client.getCustomers(MAGIC_AGE);
       assertEquals("Wrong number of client names returned.",
                     55, results.length);
}

public void testRegisterNewCustomer() throws Exception{
       ClientBean client = new ClientBean();
       //register a customer that does not already exist
       boolean couldRegister = client.register(NEW_CUSTOMER);
       assertTrue("Was not able to register " + NEW_CUSTOMER, couldRegister);
}

public void testRegisterExistingCustomer() throws Exception{
       ClientBean client = new ClientBean();

       //register a customer that DOES exist
       boolean couldNotRegister = ! client.register(EXISTING_CUSTOMER);
       String failureMessage = "Was able to register an existing customer ("
                           + EXISTING_CUSTOMER + "). This should not be " +
                           "possible."
       assertTrue(failureMessage, couldNotRegister);
}

クライアントが予想通りの結果を返した場合、テストは合格です。このテストは非常に単純ですが、EJBコンポーネントへの呼び出しに基づいて出力を生成するサーブレットなど、もっと複雑なクライアントの場合に、どのように同じ手順を適用したらよいか容易に想像できるでしょう。

もしサンプル・アプリケーションを既にインストールしてある場合には、サンプルのディレクトリーでコマンドant basic を実行することにより、このテストを何回か実行してみてください。


データに依存するテストの問題点

上記のテストを何回か実行してみると、結果が矛盾していることにお気付きになるでしょう。テストが合格になるときと、不合格になるときがあるのです。この矛盾の原因は、クライアントではなく、EJBコンポーネントの実装にあります。この例のEJBコンポーネントは、不確かなシステム状態をシミュレートしています。テスト・データの矛盾は、単純な、データ中心のテストを実装する場合に、大きな問題になります。別の重大な問題は、テスト内容が重複する傾向があることです。ここでは、その2つの問題点について説明します。

データの管理

データの不確実さを克服するための簡単な方法は、データの状態を管理することです。単体テストを実行する前に、システムに55個の顧客レコードがあることを何とかして保証できれば、getCustomers() のテストで問題があった場合、その原因がデータにではなく、コードの欠陥にあることがわかります。しかし、データの状態を管理しようとすると、また別の種類の問題が起きてきます。各テストを実行する前に、システムがその特定のテスト用の適切な状態にあることを確認する必要があります。慎重に処理を進めないと、1つのテストの結果がシステムの状態を変更してしまい、次のテストが失敗する原因になることがあります。

この心配に対処するには、共用のセットアップ・クラスを利用したり、バッチ入力プロセスを利用したりすることができます。しかし、このどちらのアプローチも、インフラストラクチャーにとってかなりの投資を意味します。また、アプリケーションの状態が何らかの種類の記憶装置に保持される場合には、さらに別の問題が起こることがあります。データを記憶装置システムに追加する処理は複雑になることがあり、挿入と削除を頻繁に繰り返すと、テストの実行が低速になる可能性があります。

上位レベルのテスト

この記事は単体テストに焦点を合わせていますが、統合テストや機能テストも、迅速な開発と高品質のために同じように重要です。実際、この2種類のテストは互いに補い合っています。上位レベルのテストはシステムのエンドツーエンドの完全性を検証するものであるのに対して、下位レベルの単体テストは個々のコンポーネントを検証するものです。どちらも、それぞれ異なる状況で有用です。たとえば、単体テストによって、まれな状況でのみ発生するバグが洗い出されたとしても、機能テストは合格になることがあります。その逆に、単体テストが合格でも、機能テストによって、個々のコンポーネントが正しく結び付けられていないことが明らかになることがあります。機能テストでは、データに依存するテストを実施する方が有意義である場合があります。その目的が、システムの全体としての振る舞いを検証することだからです。

状態管理に関する問題に遭遇するよりさらに悪いのは、そのような管理がまったく不可能な状況に遭遇することです。この種の状況は、サード・パーティーのサービスのクライアント・コードをテストする場合に直面します。読み取り専用の種類のサービスは、システムの状態を変更する機能を公開していないことがあります。あるいは、ビジネス上の理由からテスト・データを挿入するの思いとどまることもあるでしょう。たとえば、実稼動している処理キューにテスト注文を送るのは、おそらくまずいでしょう。

重複したテスト

たとえシステムの状態を完全に制御できたとしても、状態ベースのテストでは、テスト内容が重複するという望ましくない状況になることがあります。いずれにしても、同じテストを2回記述するのは避けたいと思うでしょう。

テスト・アプリケーションを例にとって考えてみましょう。CustomerManager EJBコンポーネントを制御することができ、正しく振る舞うことが確認されているテストが既に手元にあるとします。このアプリケーションのクライアント・コードは、システムに新しい顧客を追加することに関係するロジックを、実際には何も実行していません。クライアント・コードは、その操作をCustomerManager に委任するだけです。そうであれば、CustomerManager を再度テストする必要があるのでしょうか。

誰かがCustomerManager の実装を変更したため、同じデータに対して異なる応答を示すようになったとすると、この変更に対応するために、2つのテストを変更する必要があります。これには、テスト間の結合の行き過ぎの感があります。幸い、この重複は不必要です。ClientBeanCustomerManager と正しく通信していることを検証できれば、ClientBean はしかるべき動作をするはずだという十分な確証を得られます。疑似オブジェクトによるテストでは、まさにそのような種類の検証を実行できます。


疑似オブジェクトによるテスト

疑似オブジェクトは、必要以上に単体テストを行ってしまうのを防ぎます。疑似オブジェクトによるテストでは、本物のコラボレーター (共同作業者) を疑似実装で置き換えます。そして、疑似実装を使うと、テスト対象のクラスとコラボレーターが正しく相互作用していることを簡単に検証できます。その仕組みを、簡単な例で説明しましょう。

テストするコードは、クライアント/サーバーのデータ管理システムからオブジェクトのリストを削除するコードです。リスト4に、テストするメソッドを示します。



リスト4. テスト対象のメソッド
                
       public interface Deletable {
           void delete();
       }

       public class Deleter {

           public static void delete(Collection deletables){
               for(Iterator it = deletables.iterator(); it.hasNext();){
                   ((Deletable)it.next()).delete();
               }
           }
       }

素朴な単体テストでは、実際にDeletable を作成してから、Deleter.delete() を呼び出した後にそれが消滅することを検証するでしょう。しかし、疑似オブジェクトを使ってDeleter クラスをテストするには、Deletable を実装する疑似オブジェクトを記述します (リスト5を参照)。



リスト5. 疑似オブジェクトによるテスト
                
       public class MockDeletable implements Deletable{

           private boolean deleteCalled;

           public void delete(){
               deleteCalled = true;
           }

           public void verify(){
               if(!deleteCalled){
                   throw new Error("Delete was not called.");
               }
           }
       }

次に、Deleter の単体テストでは、この疑似オブジェクトを使用します (リスト6を参照)。



リスト6. 疑似オブジェクトを使用するテスト・メソッド
                
       public void testDelete() {
           MockDeletable mock1 = new MockDeletable();
           MockDeletable mock2 = new MockDeletable();

           ArrayList mocks = new ArrayList();
           mocks.add(mock1);
           mocks.add(mock2);

           Deleter.delete(mocks);

           mock1.verify();
           mock2.verify();
       }

このテストを実行すると、Deleter が、コレクション内の各オブジェクトのdelete() を正しく呼び出したことが検証されます。このようにして、疑似オブジェクトによるテストでは、テストするクラスの周囲にあるものを正確に制御して、そのコード・ユニットが周囲と正しく相互作用することを検証します。


疑似オブジェクトの限界

オブジェクト指向プログラミングでは、テストされるクラスの実行に対する疑似オブジェクトを用いたテストの影響に関して限界があります。たとえば、少し異なるdelete() メソッド (たとえば、削除可能オブジェクトを削除する前に、オブジェクトのリストをルックアップするメソッド) をテストしようとすると、疑似オブジェクトを提供するのはそれほど簡単ではなくなります。次のメソッドは、疑似オブジェクトを使ってテストするのは困難です。



リスト7. 疑似オブジェクトを作成するのが難しいメソッド
                
public static void deleteAllObjectMatching(String criteria){
           Collection deletables = fetchThemFromSomewhere(criteria);
           for(Iterator it = deletables.iterator(); it.hasNext();){
               ((Deletable)it.next()).delete();
           }
       }

疑似オブジェクトによるテストの擁護者は、上のようなメソッドは、もっと「疑似フレンドリー」になるようにリファクタリングするべきだと主張します。そのようなリファクタリングは、しばしば、より明瞭で、より柔軟な設計につながります。適切に設計されたシステムでは、各コード・ユニットがそのコンテキストと相互作用するときに、各種の実装 (疑似実装を含む) をサポートする、適切に定義されたインターフェースを使用します。

しかし、適切に設計されたシステムであっても、テストが容易にコンテキストに影響を与えることができないケースがあります。これは、何らかのグローバルにアクセス可能なリソースに対する呼び出しを含むコードの場合です。たとえば、静的メソッドの呼び出しは検証したり置き換えたりするのが困難です。また、new 演算子を使ったオブジェクトのインスタンス生成も同じです。

疑似オブジェクトは、グローバル・リソースに対しては役立ちません。疑似オブジェクトによるテストは、ドメイン・クラスを、共通のインターフェースを共有するテスト・クラスに手作業で置き換えることに依存しているからです。静的メソッドの呼び出し (および、その他の種類のグローバル・リソースのアクセス) はオーバーライドできないため、それらのメソッドに対する呼び出しは、インスタンス・メソッドと同じような仕方で「リダイレクト」することはできません。

リスト4のメソッドには任意のDeletable を渡すことができます。しかし、実際の物の代わりに異なるクラスをロードする以外は、Java言語を使って静的メソッドの呼び出しを疑似メソッドの呼び出しに置き換えることはできません。

リファクタリングの例

アプリケーション・コードをリファクタリングすると、しばしば、コードがエレガントになるとともに、容易にテストできるようになります。しかし、いつもそうなるとは限りません。テストを可能にするためにリファクタリングしても、その結果として生成されたコードの保守が大変だったり、理解しにくかったりするなら、それは無意味です。

EJBコードは、疑似オブジェクトによるテストが容易な状態にリファクタリングするのに、とりわけ奇策を必要とすることがあります。たとえば、疑似フレンドリーなリファクタリングの一例として、次のようなコードを、

//in EJBNumber1
public void doSomething(){
    EJBNumber2 collaborator = lookupEJBNumber2();
    //do something with collaborator
}

次のように書き換える、というものがあります。

public void doSomething(EJBNumber2 collaborator){
    //do something with collaborator
}

標準的なオブジェクト指向システムでは、この種のリファクタリングによって柔軟性が増します。呼び出し元がコード・ユニットにコラボレーター(協力者)を提供できるからです。しかし、このようなリファクタリングは、EJBベースのシステムでは望ましくありません。パフォーマンス上の理由から、リモートEJBクライアントは、リモート・メソッド呼び出しをできるだけ少なくする必要があるからです。上記の2番目のアプローチでは、クライアントはまずEJBNumber2 をルックアップし、次にそのインスタンスを生成する必要があり、数回のリモート・オペレーションを伴うプロセスになります。

それに加えて、適切に設計されたEJBシステムは、「階層化された」アプローチに向かう傾向があります。つまり、クライアント層では、EJBNumber2 の存在といった実装上の詳細を必ずしも知らないでもよいわけです。EJBインスタンスを取得するための望ましい方法は、JNDIコンテキストからファクトリー (Home インターフェース) をルックアップして、ファクトリー上の作成メソッドを呼び出すことです。この方法は、リファクタリングしたコード・サンプルで意図していた柔軟性を、EJBアプリケーションに与えてくれます。アプリケーションの提供者は、配備の時点でEJBNumber2 の実装をまったく異なるものに交換できるため、システムの振る舞いを容易に調整できます。しかし、JNDIバインディングは、実行時に容易に変更することはできません。したがって、疑似オブジェクトによるテストを実行したい人は、EJBNumber2 の疑似実装を交換するために再配備するか、このテスト・モデルを完全にあきらめるか、どちらかを選ぶことになります。

幸いなことに、AspectJが次善の策を提供してくれます。


AspectJによる柔軟性の追加

AspectJは、通常なら疑似オブジェクトを使用できないような状況でも、テスト・ケースごとにコンテキストに依存した振る舞いの修正を提供できます。AspectJのjoin-pointモデルでは、アスペクト と呼ばれるモジュールでプログラムの実行 (たとえば、JNDIのコンテキストからオブジェクトをルックアップするなど) のポイントを特定し、それらのポイントで実行するコードを定義できます (たとえば、ルックアップに実際に進む代わりに、疑似オブジェクトを返すなど)。

アスペクトは、プログラムの制御フロー内のポイントを、pointcut によって特定します。pointcutは、プログラムの実行内から一式のポイント (AspectJの用語ではjoinpoint という) を抽出し、アスペクトがこれらのjoinpointに対して相対的に実行されるコードを定義できるようにします。シンプルなpointcutを用いて、パラメーターが特定のシグニチャーに一致するすべてJNDIルックアップを選び出すことができます。しかし、何を実行するにしても、テスト・アスペクトがテスト・コード内で発生するルックアップだけに影響するようにしなければなりません。そのためには、cflow() pointcutを使用できます。cflow は、プログラムの実行のうち、別のjoinpointのコンテキスト内で発生するポイントをすべて抽出します。

次のコード断片は、cflow ベースのpointcutでこの記事のサンプル・アプリケーションを変更する方法を示しています。

pointcut inTest() : execution(public void ClientBeanTest.test*());
/*then, later*/ cflow(inTest()) && //other conditions

これらの行は、テスト・コンテキストを定義します。最初の行では、ClientBeanTest クラス内の、何も値を返さず、publicアクセスされる、test という語で始まるメソッドの実行すべての集合に、inTest() という名前を与えます。cflow(inTest()) という式は、そのようなメソッド実行の開始とそこからの戻りの間に発生するすべてのjoinpointを抽出します。したがって、cflow(inTest()) は、「ClientBeanTest 内のテスト・メソッドの実行中」という意味です。

サンプル・アプリケーションのテスト一組は、2つの異なる構成で、それぞれ異なるアスペクトを使って構築できます。第1の構成では、本物のCustomerManager を疑似オブジェクトで置き換えます。第2の構成では、オブジェクトは置き換えませんが、ClientBean からEJBコンポーネントに対して行われる呼び出しを選択的に置き換えます。どちらのケースでも、アスペクトは表示 (show) を管理し、クライアントがCustomerManager から予測可能な結果を確実に受け取るようにします。これらの結果をチェックすることにより、ClientBeanTest は、クライアントがEJBコンポーネントを正しく使用していることを確認できます。

アスペクトを使用してEJBルックアップを置き換える

第1の構成 (リスト8を参照) は、ObjectReplacement というアスペクトをサンプル・アプリケーションに適用します。これは、Context.lookup(String) メソッドに対するすべての呼び出しの結果を置き換える働きをします。

このアプローチでは、ClientBean が想定しているJNDI構成が利用できない環境でテスト・ケースを実行できます。つまり、コマンド行や、シンプルなAnt環境から実行できるわけです。このテスト・ケースは、EJBが実際に配備される前に (あるいは、EJBが記述される前にでさえ) 実行できます。自分の管理下にないリモート・サービスに依存している場合に、実際のサービスがテスト・コンテキストで使用可能かどうかにかかわらず、単体テストを実行できるのです。


リスト8. ObjectReplacementアスペクト
                
import javax.naming.Context;

public aspect ObjectReplacement{

       /**
        * Defines a set of test methods.
        */
       pointcut inTest() : execution(public void ClientBeanTest.*());

       /**
        * Selects calls to Context.lookup occurring within test methods.
        */
       pointcut jndiLookup(String name) :
                cflow(inTest()) &&
                call(Object Context.lookup(String)) &&
                args(name);


       /**
        * This advice executes *instead of* Context.lookup
        */
        Object around(String name) : jndiLookup(name){

           if("java:comp/env/ejb/CustomerManager".equals(name)){
               return new MockCustomerManagerHome();
           }
           else{
               throw new Error("ClientBean should not lookup any EJBs " +
                               "except CustomerManager");
           }
        }
}

pointcutjndiLookup では、前に述べたpointcutを使って、Context.lookup() に対する関係のある呼び出しを特定します。jndiLookup pointcutを定義した後は、ルックアップの代わりに実行するコードを定義できます。

"advice" について

AspectJでは、advice という用語を使って、joinpointで実行されるコードのことを表しています。ObjectReplacement アスペクトは、1ブロックのadvice (上記のコードでは青字で強調表示されている) を使用します。このadviceは基本的に、「JNDIルックアップに出会ったら、メソッド呼び出しを続ける代わるに疑似オブジェクトを返しなさい」と言っています。疑似オブジェクトがクライアントに戻ると、アスペクトの仕事は完了し、あとは疑似オブジェクトが引き継ぎます。MockCustomerManagerHome (本物のホーム・オブジェクトの代理をする) は、そのcreate() メソッドが呼び出されたとき、単に顧客管理プログラムの疑似バージョンを返します。疑似オブジェクトは、正しいポイントで合法的にプログラムに入るためにホーム・インターフェースを実装しなければならないので、疑似オブジェクトはCustomerHome の スーパーインターフェースであるEJBHome のすべてのメソッドも実装しなければなりません (リスト9を参照)。



リスト9. MockCustomerManagerHome
                
public class MockCustomerManagerHome implements CustomerManagerHome{

       public CustomerManager create() 
         throws RemoteException, CreateException {
           return new MockCustomerManager();
       }


       public javax.ejb.EJBMetaData getEJBMetaData() throws RemoteException {
           throw new Error("Mock. Not implemented.");
       }

//other super methods likewise
[...]

MockCustomerManager は、複雑ではありません。また、このクラスは、スーパーインターフェースの操作のためのスタブ・メソッドも定義し、ClientBean が使用するメソッドの単純な実装も提供します (リスト10を参照)。



リスト10. MockCustomerManager上の疑似メソッド
                
public void register(String name) NameExistsException {
     if( ! name.equals(ClientBeanTest.NEW_CUSTOMER)){
         throw new NameExistsException(name + " already exists!");
     }
}

public String[] getCustomersOver(int years) {
     String[] customers = new String[55];
     for(int i = 0; i < customers.length; i++){
         customers[i] = "Customer Number " + i;
     }
     return customers;
}return customers;
}

疑似オブジェクトとしては、このコードはあまり洗練されているとは言えません。しっかりした疑似オブジェクトであれば、テストでオブジェクトの振る舞いを簡単にカスタマイズできるようにするためのフックを提供します。しかし、この記事の例の目的からして、この疑似オブジェクトの実装は、できるだけ簡単なものにしました。

アスペクトを使ってEJBコンポーネントの呼び出しを置き換える

EJBの配備フェーズをスキップすると、開発がいくぶん容易になりますが、最終的な宛先をできるだけ忠実に複製した環境でコードをテストすることにも利点があります。アプリケーションを完全に統合し、(テストにとって絶対的に重要なコンテキストだけを置き換えた) 配備されたアプリケーションに対してテストを実行すると、構成上の問題を早期に見つけ出すことができます。これは、Cactusの背後にある哲学です。Cactusは、オープン・ソースの、サーバー・サイドのテスト・フレームワークです (「Cactusに接続する」を参照)。

サンプル・アプリケーションの下記の1つの構成では、Cactusを使用してアプリケーション・サーバーでテストを実行します。これにより、ClientManager EJBが正しく構成されており、コンテナー内の他のコンポーネントからアクセスできることを検証できます。AspectJでも、このスタイルの半統合されたテストを補完できます。その置換機能をテストで必要な振る舞いに限定し、残りのコンポーネントには影響を与えないようにすることによってです。

CallReplacement アスペクトは、テスト・コンテキストに関する同じ定義で始まります。続いて、getCustomersOver() およびregister() の各メソッドに対応するpointcutを指定します (リスト11を参照)。



リスト11. CustomerManagerに対するテスト呼び出しを選択する
                
public aspect CallReplacement{

       pointcut inTest() : execution(public void ClientBeanTest.test*());

       pointcut callToRegister(String name) :
                       cflow(inTest()) &&
                       call(void CustomerManager.register(String)) &&
                       args(name);

       pointcut callToGetCustomersOver() :
                       cflow(inTest()) &&
                       call(String[] CustomerManager.getCustomersOver(int));
       //[...]

アスペクトでは、次に、関係するメソッド呼び出しのそれぞれについて、around adviceを定義します。ClientBeanTest 内でgetCustomersOver() またはregister() への呼び出しが発生すると、関係するadviceが代わりに実行されます (リスト12を参照)。



リスト12. adviceによりテスト内のメソッド呼び出しを置き換える
                
       void around(String name) throws NameExistsException:
callToRegister(name) {
           if(!name.equals(ClientBeanTest.NEW_CUSTOMER)){
               throw new NameExistsException(name + " already exists!");
           }

       }

       Object around() : callToGetCustomersOver() {
           String[] customers = new String[55];
           for(int i = 0; i < customers.length; i++){
               customers[i] = "Customer Number " + i;
           }
           return customers;
       }

この第2の構成では、テスト・コードがいくぶん簡略化されました (別個の疑似クラスや、実装されていないメソッドのためのスタブが必要ないことに注目してください)。


交換可能なテスト構成

AspectJでは、これら2つの構成を、合図ひとつで切り替えることができます。アスペクトは、アスペクトについて何も知らないクラスに影響を及ぼせるため、コンパイル時に異なるアスペクト一式を指定すれば、システムは実行時にまったく違う振る舞いをすることになります。サンプル・アプリケーションでは、このことを活用しています。呼び出し置換バージョンとオブジェクト置換バージョンを構築する2つのAntターゲットは、下に示すとおり、ほとんど同じです。


リスト13. 異なる構成用のAntターゲット
                
       <target name="objectReplacement" description="...">
         <antcall target="compileAndRunTests">
           <param name="argfile"
                        value="${src}/ajtest/objectReplacement.lst"/>
         </antcall>
       </target>

       [contents of objectReplacement.lst]
       @base.lst;[A reference to files included in both configurations]
       MockCustomerManagerHome.java
       MockCustomerManager.java
       ObjectReplacement.java.

       <target name="callReplacement" description="...">
         <antcall target="deployAndRunTests">
           <param name="argfile"
                        value="${src}/ajtest/callReplacement.lst"/>
         </antcall>
       </target>

       [contents of callReplacement.lst]
       @base.lst
       CallReplacement.java
       RunOnServer.java

Antスクリプトは、argfile プロパティーをAspectJコンパイラーに渡します。AspectJコンパイラーは、そのファイルを使って、どのソース (Javaクラスとアスペクトの両方) をビルドに組み込むかを判別します。argfileobjectReplacement からcallReplacement に変更すれば、単に再コンパイルするだけで、ビルドのテスト方法を変更できます。

Cactusに接続する

サンプル・アプリケーションにはCactusがバンドルされています。サンプル・アプリケーションはCactusを使用して、テストをアプリケーション・サーバー内で実行します。Cactusを使用するためには、テスト・クラスがorg.apache.cactus.ServletTestCase を拡張するようにしなければなりません (junit.framework.TestCase の代わりに)。この基本クラスは、自動的に、アプリケーション・サーバーに配備されたテストと話します。"callReplacement" バージョンのテストはサーバーを必要としますが、"objectReplacement" バージョンは必要としません。そこで、私は、AspectJのもう1つの機能 (イントロダクション と呼ばれる) を利用して、テスト・クラスをサーバー対応にしました。ソース・バージョンのClientBeanTest は、TestCase を拡張しています。テストをサーバー・サイドで実行したい場合は、ビルドの構成に次のようなアスペクトを追加します。

public aspect RunOnServer{
declare parents : ClientBeanTest extends ServletTestCase;
}

このアスペクトを組み込むことにより、ClientBeanTestTestCase ではなくServletTestCase を拡張するように宣言しています。見事だと思いませんか?

Cactusについてさらに学ぶには、「参考文献」セクションを参照してください。

アスペクトをこのようにしてコンパイル時に差し替えることは、アスペクトの助けを借りたテストの場合に非常に重宝します。理想的には、実動環境には、テスト・コードの痕跡をまったく配備しないようにしたいものです。テスト用のアスペクトがコード内に侵入するタイプのものであったり、振る舞いを複雑に変更するものであったりしても、コンパイル時にアスペクトを切り離すだけで、テスト用の道具を即座に除去することができます。


結論

テストの開発コストを低く抑えるには、単体テストを単独で実行できなければなりません。疑似オブジェクトによるテストでは、テスト対象のクラスが依存しているコードの疑似的な実装を提供することにより、各コード・ユニットを分離します。しかし、グローバルにアクセス可能なソースに由来する依存関係がある状況では、協調関係にあるコードを、オブジェクト指向の技法でうまく置き換えることができません。テスト対象のコードの構造を横断できるというAspectJの機能を利用すると、そのような種類の状況でも整然とコードを置き換えることができます。

AspectJは確かに新しいプログラミング・モデル (アスペクト指向プログラミング) を導入しますが、この記事で取り上げた技法は、容易にマスターできるものです。これらの方法を利用すれば、漸進的に単体テストを記述することができ、体系的なデータを管理する必要なしに、コンポーネントの検証を適切に実行できます。


参考文献

AspectJ
  • この強力な言語拡張の可能性と使い方の紹介については、AspectJに関するNicholas Lesiecki氏の最初の記事「アスペクト指向プログラミングで、モジュール性を改善する」(developerWorks、2002年1月) をお調べください。

  • AspectJおよび関連ツールを、www.aspectj.org からダウンロードできます。このサイトには、FAQ、メーリング・リスト、優れたドキュメンテーション、他のAOP資料へのリンクもあります。

単体テストのツールと技法
  • JUnit.org は、このポピュラーな単体テスト・フレームワークに関心のある人が、手始めに情報を収集するのに最適のサイトです。このフレームワークの多くの拡張機能や、単体テスト全般に関する記事や資料も入手できます。

  • 疑似オブジェクトを使用しない場合でも、少なくともObjectMotherパターン (PDF) を使用すれば、テスト状態を作成および破棄するのに役立ちます。ObjectMotherに関するこの紹介資料は、この記事で紹介されているのとは違った観点で書かれています。

  • Cactus プロジェクトは、サーバー・サイドのコードを簡単にテストできるようにします。Cactusチームは、CactusとAspectJを統合する方法を示すサンプル・アプリケーションを近日中に公開する予定です。ご期待ください。

その他の参考文献

著者について

Nicholas Lesiecki氏は、ドットコム・ブームの時期にJavaプログラミングの世界に入り、それ以来、XPおよびJavaコミュニティーで頭角を現してきました。XPのような機敏なプロセスでオープン・ソース構築を活用し、ツールをテストするためのマニュアル 「Java Tools for Extreme Programming」 を手掛けました。 Tucson JUG に頻繁に登場するほか、JakartaのCactus プロジェクト (サーバー側単体テスト・フレームワーク) にも力を注いでいます。Nickの連絡先はndlesiecki@apache.org です。

不正使用の報告のヘルプ

不正使用の報告

ありがとうございます。 このエントリーは、モデレーターの注目フラグが設定されました。


不正使用の報告のヘルプ

不正使用の報告

不正使用の報告の送信に失敗しました。


developerWorks: サイン・イン


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 利用条件

 


お客様が developerWorks に初めてサインインすると、プロフィールが作成されます。 プロフィールで選択した情報は公開されますが、いつでもその情報を編集できます。 お客様の姓名(非表示設定にしていない限り)とディスプレイ・ネームは、投稿するコンテンツと一緒に表示されます。

表示名をお選びください

developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

(半角英数字で3文字以上31文字以下にする必要があります)


「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 利用条件

 


この記事を評価する

コメント

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219027
ArticleTitle=AspectJおよび疑似オブジェクトによる柔軟なテスト
publish-date=05012002
author1-email=ndlesiecki@apache.org
author1-email-cc=

タグ

Help
このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。

スライダーバーを使用することで、より多く(少なく)タグを表示します。

人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。

マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。

このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。