最近のエクストリーム・プログラミング (XP) への関心の高まりは、最も採用しやすいプラクティス(実践術)に向けられています。それは、単体テストとテストファースト型の設計です。ソフトウェア・メーカーがXPのプラクティスを採用するにつれて、多くの開発者は、広範囲にわたる単体テストの道具立てを手にすることにより、品質と開発スピードが向上するのを目にしてきました。しかし、優れた単体テストを記述するには、時間と労力が掛かります。それぞれのプログラム単位は相互に関連し合っているため、単体テストを記述することには、かなりの量のセットアップ(準備)・コードが必要となることがあります。このことがテストの費用を押し上げ、場合によっては (リモート・システムに対するクライアントとして動作するコードの場合など)、そのようなテストを実装するのがほとんど不可能なこともあります。
XPでは、単体テストは、統合テストと受け入れテストを補うものです。後者の2種類のテストは、別個のチームが実施したり、別個の作業として実施したりすることができます。しかし、単体テストは、テストするコードと同時に記述します。差し迫った締め切りに追われ、単体テストが頭痛の種になると、場当たり的にいいかげんなテストを記述する誘惑にかられたり、テストにまったく煩わされずにいたいと思ったりすることがあります。XPは積極的な動機と自立的なプラクティスに依存しているため、テストに焦点を合わせ、テストを記述しやすくすることは、XPプロセスにとって (そしてプロジェクトにとって) 最も注目すべきことです。
このジレンマを解決するには、疑似 (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 とは違って、ClientBean のregister() メソッドは、顧客が既に存在する場合に例外をスローするのではなく、ブール値を返します。これらは、優れた単体テストであれば検査するべき関数です。
リスト3のコードは、JUnitによるClientBean の単体テストを実装しています。テスト・メソッドは3つあります。1つはgetCustomers() 用で、あと2つはregister() 用です (1つは成功、もう1つは失敗)。このテストは、getCustomers() が55項目のリストを返し、register() は、EXISTING_CUSTOMER に対してはfalse、NEW_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回記述するのは避けたいと思うでしょう。
テスト・アプリケーションを例にとって考えてみましょう。CustomerManager EJBコンポーネントを制御することができ、正しく振る舞うことが確認されているテストが既に手元にあるとします。このアプリケーションのクライアント・コードは、システムに新しい顧客を追加することに関係するロジックを、実際には何も実行していません。クライアント・コードは、その操作をCustomerManager に委任するだけです。そうであれば、CustomerManager を再度テストする必要があるのでしょうか。
誰かがCustomerManager の実装を変更したため、同じデータに対して異なる応答を示すようになったとすると、この変更に対応するために、2つのテストを変更する必要があります。これには、テスト間の結合の行き過ぎの感があります。幸い、この重複は不必要です。ClientBean がCustomerManager と正しく通信していることを検証できれば、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の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コンポーネントを正しく使用していることを確認できます。
第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を定義した後は、ルックアップの代わりに実行するコードを定義できます。
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クラスとアスペクトの両方) をビルドに組み込むかを判別します。argfile をobjectReplacement からcallReplacement に変更すれば、単に再コンパイルするだけで、ビルドのテスト方法を変更できます。
アスペクトをこのようにしてコンパイル時に差し替えることは、アスペクトの助けを借りたテストの場合に非常に重宝します。理想的には、実動環境には、テスト・コードの痕跡をまったく配備しないようにしたいものです。テスト用のアスペクトがコード内に侵入するタイプのものであったり、振る舞いを複雑に変更するものであったりしても、コンパイル時にアスペクトを切り離すだけで、テスト用の道具を即座に除去することができます。
テストの開発コストを低く抑えるには、単体テストを単独で実行できなければなりません。疑似オブジェクトによるテストでは、テスト対象のクラスが依存しているコードの疑似的な実装を提供することにより、各コード・ユニットを分離します。しかし、グローバルにアクセス可能なソースに由来する依存関係がある状況では、協調関係にあるコードを、オブジェクト指向の技法でうまく置き換えることができません。テスト対象のコードの構造を横断できるというAspectJの機能を利用すると、そのような種類の状況でも整然とコードを置き換えることができます。
AspectJは確かに新しいプログラミング・モデル (アスペクト指向プログラミング) を導入しますが、この記事で取り上げた技法は、容易にマスターできるものです。これらの方法を利用すれば、漸進的に単体テストを記述することができ、体系的なデータを管理する必要なしに、コンポーネントの検証を適切に実行できます。
AspectJ
- この強力な言語拡張の可能性と使い方の紹介については、AspectJに関するNicholas Lesiecki氏の最初の記事「アスペクト指向プログラミングで、モジュール性を改善する」(developerWorks、2002年1月) をお調べください。
- AspectJおよび関連ツールを、www.aspectj.org からダウンロードできます。このサイトには、FAQ、メーリング・リスト、優れたドキュメンテーション、他のAOP資料へのリンクもあります。
-
JUnit.org は、このポピュラーな単体テスト・フレームワークに関心のある人が、手始めに情報を収集するのに最適のサイトです。このフレームワークの多くの拡張機能や、単体テスト全般に関する記事や資料も入手できます。
- 疑似オブジェクトを使用しない場合でも、少なくともObjectMotherパターン (PDF) を使用すれば、テスト状態を作成および破棄するのに役立ちます。ObjectMotherに関するこの紹介資料は、この記事で紹介されているのとは違った観点で書かれています。
-
Cactus プロジェクトは、サーバー・サイドのコードを簡単にテストできるようにします。Cactusチームは、CactusとAspectJを統合する方法を示すサンプル・アプリケーションを近日中に公開する予定です。ご期待ください。
- この記事のサンプル・アプリケーションをダウンロードできます。
- サンプル・アプリケーションは、Jakartaから無料で入手できるAnt を使って構築しました。
- developerWorks では、AntとJUnitの入門として役立つ優れた記事を取り上げました。Erik Hatcher氏の「Automating the build and test process」(2001年8月) と、Malcolm Davis氏の「AntとJUnitを用いた漸進的開発」(2000年11月) を参照してください。
- エクストリーム・プログラミングのプラクティスと方法論の詳細については、XProgramming.com を参照してください。
- Javaプログラミングの様々な側面についてのたくさんの記事が、IBMdeveloperWorks のJavaテクノロジー・ゾーン に収録されています。
Nicholas Lesiecki氏は、ドットコム・ブームの時期にJavaプログラミングの世界に入り、それ以来、XPおよびJavaコミュニティーで頭角を現してきました。XPのような機敏なプロセスでオープン・ソース構築を活用し、ツールをテストするためのマニュアル 「Java Tools for Extreme Programming」 を手掛けました。 Tucson JUG に頻繁に登場するほか、JakartaのCactus プロジェクト (サーバー側単体テスト・フレームワーク) にも力を注いでいます。Nickの連絡先はndlesiecki@apache.org です。