レベル: 初級 Andrew Glover (aglover@stelligent.com), President, Stelligent Incorporated
2007年 7月 24日 Ajax アプリケーションを作るのはワクワクすることですが、そのアプリケーションのユニット・テストには実に四苦八苦します。今回の記事で Andrew Glover が取り上げるのは、そんな Ajax のマイナス面 (マイナス面のひとつである)、非同期 Web アプリケーション特有のユニット・テストの問題です。彼が発見したように、Google Web Toolkit の助けを借りれば、この非同期 Web アプリケーションのコード品質の問題を思ったより簡単に解決することができます。
最近の記憶のなかで巷の Web 開発を揺るがせた話題と言えば、間違いなく Ajax が挙げられます。Ajax 関連のツール、フレームワーク、本、そして Web サイトの急増がこの技術の普及を証明しているだけでなく、ご存知のとおり、Ajax アプリケーションはかなり俗受けする技術です。しかし、Ajax アプリケーションを開発したことのある誰もが証言するように、Ajax のテストとなるとそうは簡単に行きません。実際、Ajax の出現によって、非同期 Web アプリケーションのテスト用に設計されてはいなかった多くのテスト・フレームワークやツールが役に立たなくなってしまいました。
面白いことに、ある Ajax 対応フレームワークの開発者たちはこの制約に気付き、今までにない奇抜な方法で対処しました。それは、テスト容易性を組み込んだことです。さらに、このフレームワークはJavaScript ではなく Java™ コードを使って Ajax アプリケーションの作成を容易にするので、彼らはいわば大物たちに依存して、Java プラットフォームの標準テスト・フレームワークとも言える JUnit を利用したのです。
ここで話しているフレームワークとは、もちろん、絶大な人気を誇る Google Web Toolkit です (GWT としても知られています)。 この記事では、GWT では実際に Java 互換性をどのように利用して、同期アプリケーションと同じく Ajax アプリケーションのあらゆる部分をテストできるようにしているのかを説明します。
JUnit と GWTTestCase
GWT 関連の Ajax アプリケーションは Java コードで作成されるため、JUnit を使ってテストを行う開発者にはこの上なく向いています。事実、GWT 開発者チームは JUnit の 3.8.1 TestCase を拡張して GWTTestCase という名前のヘルパー・クラスを作成しました。この基本クラスは、GWT コードをテストするための機能のほかに、GWT コンポーネントを起動して実行させるための処理機能も追加します。
 |
The Google Web Toolkit
Google Web Toolkit は派手な宣伝とともに Java Web 開発コミュニティーに発表され、その宣伝に見合った注目を集めました。GWT は Ajax 対応の Web アプリケーションを Java コードを利用して設計、ビルド、デプロイするという革新的な方法を実現します。GWT を使用すれば、Java Web 開発者は JavaScript を学んで複雑なブラウザー固有の問題に何時間も費やす代わりに、すぐさま Ajax に関連付けられた動的で情報豊富な Web アプリケーションの設計に取り掛かれます。
|
|
1 つ念頭に置いておかなければならないのは、GWTTestCase は UI 関連のコードをテストするようには意図されていないという点です。このクラスは、UI 操作によってトリガーできる非同期の側面をテストしやすくすることを目的としています。GWT に馴染みのない開発者のなかには、このような GWTTestCase の目的を誤解し、ユーザー操作を簡単に模倣できるという期待が裏切られて不満を持つ人も少なくありません。
基本的に、Ajax コンポーネントには 2 つの側面があります。1 つはそのルック・アンド・フィール、そしてもう 1 つは当然非同期なものとして設計された機能です。図 1 は、Web フォームをエミュレートする単純な Ajax コンポーネントです。このコンポーネントは Ajax に対応しているため、フォームの実行依頼は非同期で行われます (つまり、従来のフォーム実行依頼に伴うページのリロードは行われません)。
図 1. 単純な Ajax 対応 Web フォーム
有効な単語を入力してから、このコンポーネントの Submit ボタンをクリックすると、その単語の定義を要求するメッセージがサーバーに送信されます。単語の定義はコールバックによって非同期に返され、適宜 Web ページに挿入されます。その結果が図 2 です。
図 2. Submit ボタンをクリックすると表示される応答
機能テストと結合テスト
図 2 に示した対話動作をテストするには、さまざまなシナリオが考えられますが、なかでも代表的なシナリオは 2 つあります。まず機能の観点から、フォームに値を入力し、Submit ボタンをクリックするとフォームの下に定義が表示されることを検証するためのテストを作成するというシナリオです。2 つ目は結合テストで、このテストではクライアント・サイド・コードの非同期機能を確認できるようにします。GWT の GWTTestCase は、後者のテスト専用に設計されています。
覚えておかなければならない点として、GWTTestCase テスト・ケース環境ではユーザー操作によるテストは不可能です。そのため GWT アプリケーションを設計、ビルドする際にはユーザー操作に頼らずにコードをテストすることを考える必要があります。このように考えるには、相互作用コードをビジネス・ロジックから分離することが不可欠です。手始めとしては、このような分離が (ご存知のとおり) ベスト・プラクティスとなります。
例えば、図 1 と図 2 の Ajax アプリケーションを別の観点から見てください。このアプリケーションを構成している 4 つの論理コンポーネントは、目的のワードを入力する TextBox 、クリックする Button 、そして 2 つの Label (TextBox 用に 1 つと、定義が表示される場所に 1 つ) です。実際の GWT モジュールでの最初の取り組みではリスト 1 のようなコードが考えられますが、このコードをテストするとなると問題があります
リスト 1. 有効ながらもテストする上では問題がある GWT アプリケーション
public class DefaultModule implements EntryPoint {
public void onModuleLoad() {
Button button = new Button("Submit");
TextBox box = new TextBox();
Label output = new Label();
Label label = new Label("Word: ");
HorizontalPanel inputPanel = new HorizontalPanel();
inputPanel.setStyleName("input-panel");
inputPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
inputPanel.add(label);
inputPanel.add(box);
button.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
String word = box.getText();
WordServiceAsync instance = WordService.Util.getInstance();
try {
instance.getDefinition(word, new AsyncCallback() {
public void onFailure(Throwable error) {
Window.alert("Error occurred:" + error.toString());
}
public void onSuccess(Object retValue) {
output.setText(retValue.toString());
}
});
}catch(Exception e) {
e.printStackTrace();
}
}
});
inputPanel.add(button);
inputPanel.setCellVerticalAlignment(button,
HasVerticalAlignment.ALIGN_BOTTOM);
RootPanel.get("slot1").add(inputPanel);
RootPanel.get("slot2").add(output);
}
}
|
リスト 1 のコードは正常に機能しますが、大きな欠点があります。それは、このコードは JUnit や GWT の GWTTestCase ではテストできないという点です。私が実際にこのコードのテストを作成してみたところ、技術的には実行できても論理的に機能するテストにはなりませんでした。ここで落ち着いて考えてみてください。このコードの何を検証できるのでしょうか。テストに使用できる唯一の public メソッドは void を返すため、それが正しく機能するかどうかを検証する手立てはありません。
このコードをホワイト・ボックス方式で検証しようと思ったら、UI 固有のコードをビジネス・コードから分離しなければなりません。それにはリファクタリングが必要になります。要するに、リスト 1 のコードの内容を、簡単にテストできる個別のメソッドに入れるということです。ただし、これは思うほど簡単な作業ではありません。コンポーネントのフックは明らかに onModuleLoad() メソッドを使用していますが、その動作を強制するには一部の UI コンポーネントを操作せざるを得ない可能性があるからです。
ビジネス・ロジックからの UI コードの分離
最初のステップとして、UI コンポーネントごとにアクセサー・メソッドを作成します (リスト 2 を参照)。このようにすると、必要に応じて UI コンポーネントを取得できるようになるからです。
リスト 2. UI コンポーネントをアクセス可能にするためのアクセサー・メソッドの追加
public class WordModule implements EntryPoint {
private Label label;
private Button button;
private TextBox textBox;
private Label outputLabel;
protected Button getButton() {
if (this.button == null) {
this.button = new Button("Submit");
}
return this.button;
}
protected Label getLabel() {
if (this.label == null) {
this.label = new Label("Word: ");
}
return this.label;
}
protected Label getOutputLabel() {
if (this.outputLabel == null) {
this.outputLabel = new Label();
}
return this.outputLabel;
}
protected TextBox getTextBox() {
if (this.textBox == null) {
this.textBox = new TextBox();
this.textBox.setVisibleLength(20);
}
return this.textBox;
}
}
|
これで、すべての UI 関連コンポーネントにプログラマチックによってアクセスできるようになります (アクセスを必要とするすべてのクラスが同じパッケージに含まれているという前提です)。これらのアクセサー・メソッドのいずれかを実際の検証で使用しなければならないかどうかはそのうちわかることですが、なるべくなら使用したくありません。前述したように、GWT は相互作用のテスト用ではないからです。このサンプルでテストしようとしているのは、ボタン・インスタンスがクリックされたかどうかではなく、GWT モジュールが特定の単語を対象にサーバー・サイド・コードを呼び出すこと、そしてサーバー・サイドが有効な定義を返すことです。そこで、onModuleLoad() メソッドの定義検索ロジックを、テスト可能なメソッドに組み込むことにします (リスト 3 を参照)。
リスト 3. テストしやすいメソッドに任せるようにリファクタリングされた onModuleLoad メソッド
public void onModuleLoad() {
HorizontalPanel inputPanel = new HorizontalPanel();
inputPanel.setStyleName("disco-input-panel");
inputPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
Label lbl = this.getLabel();
inputPanel.add(lbl);
TextBox txBox = this.getTextBox();
inputPanel.add(txBox);
Button btn = this.getButton();
btn.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
submitWord();
}
});
inputPanel.add(btn);
inputPanel.setCellVerticalAlignment(btn,
HasVerticalAlignment.ALIGN_BOTTOM);
if(RootPanel.get("input-container") != null) {
RootPanel.get("input-container").add(inputPanel);
}
Label output = this.getOutputLabel();
if(RootPanel.get("output-container") != null) {
RootPanel.get("output-container").add(output);
}
}
|
リスト 3 を見るとわかるように、基本的に onModuleLoad() の定義検索ロジックは、以下のリスト 4 に定義する submitWord メソッドに委ねています。
リスト 4. Ajax アプリケーションの内容
protected void submitWord() {
String word = this.getTextBox().getText().trim();
this.getDefinition(word);
}
protected void getDefinition(String word) {
WordServiceAsync instance = WordService.Util.getInstance();
try {
instance.getDefinition(word, new AsyncCallback() {
public void onFailure(Throwable error) {
Window.alert("Error occurred:" + error.toString());
}
public void onSuccess(Object retValue) {
getOutputLabel().setText(retValue.toString());
}
});
}catch(Exception e) {
e.printStackTrace();
}
}
|
submitWord() メソッドは、JUnit を使ってテストできる getDefinition() という別のメソッドに従います。この getDefinition() メソッドは論理的に UI 固有のコードから (大部分は) 切り離されているため、ボタン・クリックとは関係なく呼び出すことができます。その一方で、非同期アプリケーションに関する状態の問題、そして Java 言語のセマンティック・ルールにより、このテストで UI 関連の対話動作を完全に避けて通ることはできません。リスト 4 のコードをよく見てみると、非同期コールバックを呼び出す getDefinition() メソッドが一部の UI コンポーネント、つまりエラーのアラート・ウィンドウと Label インスタンスを操作していることがわかります。
それでもまだ、出力 Label インスタンスのハンドルを取得し、そのテキストが指定された単語の定義であることを表明するという方法でアプリケーションの機能性を検証することはできます。GWTTestCase によってテストするときは、手作業で状態の変更をコンポーネントに強要しようとするよりも、GWT に任せるのが一番です。例えばリスト 4 では、正しい定義が返され、指定した単語の出力 Label に配置されることを検証しようとしていますが、UI コンポーネントを実際に操作して単語を設定することはしていません。getDefinition メソッドを直接呼び出して、Label に対応する定義があることを表明するだけで済んでいます。
テストを念頭に置いて GWT アプリケーションを作成したので、次は実際にアプリケーションのテストを作成する必要があります。つまり、GWT の GWTTestCase をセットアップします。
GWTTestCase のセットアップ
GWTTestCase のテストの威力を利用するには、従うべきルールがいくつかあります。幸い、これらのルールは以下のように単純なものです。
- すべての実装テスト・クラスが、テスト対象の GWT モジュールと同じパッケージ内にあること。
- テストを実行する際には少なくとも 1 つの VM 引数を渡して、テストの実行に使用する GWT モード (ホスト・モードまたは Web モード) を指定できるようにすること。
- XML モジュール・ファイルの String 表現を返す
getModuleName() メソッドを実装すること。
最後につけ加えると、サーバー・サイド・エンティティーとやり取りする Ajax アプリケーションは本来非同期なので、GWT には追加の Timer クラスが用意されています。このクラスは、非同期動作が完了してから関連する表明が行われるように、JUnit を遅延させるうえで役立ちます。
getModuleName と Timer クラスの実装
前述のとおり、このテストに関する作業の主な部分は getDefinition() メソッドに集中しています (リスト 4 を参照)。このコードからわかるように、テストは論理的には単純なもので、単語 (pugnacious など) を渡した後、対応する Label のテキストとして正しい定義が表示されることを検証します。このように単純なテストですが、getDefinition() メソッドには AsyncCallback オブジェクトによって関連付けられた非同期の性質があることを忘れないでください。
GWTTestCase クラスは、このクラスが持つ getModuleName() が抽象メソッドとして宣言されていることから抽象クラスとなります。したがって、このクラスを拡張するときには getModuleName() メソッドを実装しなければなりません (ただし、フレームワークに独自の基本抽象クラスを作成する場合を除きます)。モジュール名は基本的に、GWT XML ファイルが存在するパッケージ構造からファイル拡張子を除いた名前です。例えば、このサンプルでは WordModule.gwt.xml という XML ファイルが com/acme/gwt というディレクトリー構造にあるため、モジュールの論理名は com.acme.gwt.WordModule となります。この名前の付け方は、Java プラットフォームの標準的なパッケージングを思い出すことと思います。
モジュール名がわかったところで、早速リスト 5 のテスト・ケースの定義に取り掛かります。
リスト 5. getModuleName メソッドの実装と有効な名前の指定
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.Timer;
public class WordModuleTest extends GWTTestCase {
public String getModuleName() {
return "com.acme.gwt.WordModule";
}
}
|
ここまでのところは順調ですが、まだ何もテストしていません。この Ajax アプリケーションでは AsyncCallback オブジェクトを使用するため、テスト・ケースによって getDefinition() メソッドを呼び出したときに、JUnit に強制して実行が完了するのを遅らせなくてはなりません。そうしないと、応答がないためテストが失敗するからです。ここで役立つのが、GWT の Timer クラスです。Timer クラスを使用すれば、getDefinition() の run メソッドをオーバーライドし、Timer の期間内でテスト・ケースのロジックを終了させることができます (テスト・ケースは別のスレッドで実行されるため、JUnit を効果的にブロックしてテスト・ケース全体を完了させないようにします)。
例えば私が行うテストでは、まず getDefinition() メソッドを呼び出してから Timer の run() メソッド実装を提供します。この run() メソッドが出力 Label インスタンスのテキストを取得し、そこに適切な定義があることを検証します。Timer インスタンスを定義したら、次にこのインスタンスの起動をスケジュールすると同時に、JUnit が Timer インスタンスの完了まで待機するようにしなければなりません。複雑そうに聞こえるかもしれませんが、実際は至って簡単なので心配しないでください。リスト 6 のシナリオ全体を見ればわかることです。
リスト 6. GWT による簡単なテスト
public void testDefinitionValue() throws Exception {
WordModule module = new WordModule();
module.getDefinition("pugnacious");
Timer timer = new Timer() {
public void run() {
String value = module.getOutputLabel().getText();
String control = "inclined to quarrel or fight readily;...";
assertEquals("should be " + control, control, value);
finishTest();
}
};
timer.schedule(200);
delayTestFinish(500);
}
|
ご覧のように、実際に Ajax アプリケーションの機能とそのリモート・プロシージャー・コールの使用を検証している場所は Timer の run() メソッドです。run メソッドの最後のステップでは finishTest() メソッドを呼び出していることに注目してください。このメソッドが、すべてが正常に完了したため、JUnit のブロックを解除して通常通りに実行させるよう指示するわけです。実際に実行してみると、非同期動作の完了に必要な時間に応じて遅延時間を調整する必要が出てくるかもしれません。しかし、JUnit を使って GWT アプリケーションをテストする上での要点は、テストのために、完全に機能する Web アプリケーションをデプロイする必要がないことです。そのため、GWTアプリケーションをすぐに、そして願わくば、より頻繁にテストできるようになります。
GWT テストの実行
 |
GWT による機能テスト
この記事に記載したような単純な Ajax アプリケーションは、Selenium などのフレームワークを使用して機能の点から検証することもできます。Selenium はブラウザーを操作して実際のユーザーの操作をシミュレートするフレームワークです。ただし Selenium を使用して機能テストを実行するには、完全に機能する Web アプリケーションをデプロイしなければなりません。
|
|
前にも述べたように、実際に GWT JUnit テストを実行する場合は、実行環境を構成するための細々とした作業が必要になります。例えば Ant の junit タスクによってテストを実行するには、いくつかの特定のファイルがクラスパス内にあることを確認し、基礎となる JVM に引数を渡さなければなりません。具体的に言うと、junit タスクを呼び出すときに、ソース・ファイルを収容するディレクトリー (または複数のディレクトリー) がクラスパスに組み込まれていることを確認し、GWT に実行モードを指示するということです。私が使うようにしているのはホスト・モードで、つまり www-test フラグを使用します (リスト 7 を参照)。
リスト 7. Ant による GWT テストの実行
<junit dir="./" failureproperty="test.failure" printSummary="yes"
fork="true" haltonerror="true">
<jvmarg value="-Dgwt.args=-out www-test" />
<sysproperty key="basedir" value="." />
<formatter type="xml" />
<formatter usefile="false" type="plain" />
<classpath>
<path refid="gwt-classpath" />
<pathelement path="build/classes" />
<pathelement path="src" />
<pathelement path="test" />
</classpath>
<batchtest todir="${testreportdir}">
<fileset dir="test">
<include name="**/**Test.java" />
</fileset>
</batchtest>
</junit>
|
ここからは、GWT テストを実行するのはビルドを呼び出すだけの話となります。もう 1 つ注目してもらいたいのは、GWT テストはかなり軽量だという点です。そのため、テストを頻繁に実行することも、あるいは Continuous Integration 環境での場合のように継続的に実行することもできます。
まとめ
この記事で実演した GWT テスト・ケースでは、Ajax アプリケーションが期待どおりに実行することを確認するための初歩な的方法を説明しました。このサンプル GWT アプリケーションで、例えばもう少し制約のあるケースをテストするなど引き続きテストを行うこともできますが、すでに要点は抑えたと思います。つまり、テスト機能を統合するフレームワークを使ってテストを作成すれば、思ったより簡単に Ajax アプリケーションをテストできるということです。
GWT アプリケーションのテストでも同じく重要なのは (ほとんどのアプリケーションに当てはまるように)、テストを念頭に置いてアプリケーションを設計することです。また、GWTTestCase は相互作用のテスト用ではないことも覚えておいてください。GWTTestCase を使ってユーザーを直接シミュレートすることはできません。ただし、この記事で説明したように、GWTTestCase で間接的にユーザー操作を検証することは可能です。
参考文献
著者について
記事の評価
|