Web アプリケーションにとって最大の難題の 1 つは、各種 Web ブラウザーのさまざまなバージョンをサポートしなければならないという問題です。Safari で正常に実行される JavaScript コードが必ずしも Windows Internet Explorer (IE) や Firefox、Google Chrome で正常に動作するとは限りません。この問題の原因は、プレゼンテーション層に存在する JavaScript コードを最初の段階でテストしていないことにあります。この JavaScript コードに対してユニット・テストを実行しない場合には、組織は新しいブラウザーにアップグレードした後、または新しいブラウザーをサポートした後、Web アプリケーションのテストを繰り返すために費用をかけることになります。この記事では、JavaScript コードのユニット・テストを効率的に行うことによってテスト・コストを削減する方法を説明します。
JavaScript を使用したよくある例のひとつが、ログイン・フォームを JavaScript で検証する例です。リスト 1 のフォームを考えてみてください。
リスト 1. ログイン・フォーム
<FORM> <table> <tr> <td>Username</td> <td><input type="text" id="username"/></td> <td><span id="usernameMessage"></span></td> </tr> <tr> <td>Password</td> <td><input type="password" id="password"/></td> <td><span id="passwordMessage"></span></td> </tr> <tr> <td><input type="button" onclick="new appnamespace. ApplicationUtil().validateLoginForm()" value="Submit"/></td> </tr> </table> </FORM> |
このフォームは単純です。このフォームはユーザー名フィールドとパスワード・フィールドで構成されています。送信ボタンをクリックすると、このフォームに特有の検証が ApplicationUtil によって行われます。ApplicationUtil は、この HTML フォームを検証する
JavaScript オブジェクトです。リスト 2 は、この ApplicationUtil オブジェクトのコードを示しています。
リスト 2. 問題のある ApplicationUtil オブジェクトのコード
appnamespace = {};
appnamespace.ApplicationUtil = function() {};
appnamespace.ApplicationUtil.prototype.validateLoginForm = function(){
var error = true;
document.getElementById("usernameMessage").innerText = "";
document.getElementById("passwordMessage").innerText = "";
if (!document.getElementById("username").value) {
document.getElementById("usernameMessage").innerText =
"This field is required";
error = false;
}
if (!document.getElementById("password").value) {
document.getElementById("passwordMessage").innerText =
"This field is required";
error = false;
}
return error;
};
|
リスト 2 で、ApplicationUtil
オブジェクトは単純な検証を行っています。このオブジェクトはユーザー名フィールドとパスワード・フィールドに値が入力されているかどうかを検証しています。フィールドが空の場合、「This field is required (このフィールドは必須)」というエラー・メッセージが表示されます。
リスト 2 のコードは Internet Explorer 8 と Safari 5.1 では動作しますが、Firefox 3.6 では動作しません。Firefox では
innerText プロパティーがサポートされていないからです。多くの場合、(上記のコードや、それと似た他の
JavaScript コードでの) 主な問題は、作成された JavaScript コードがクロスブラウザー対応であるかどうかが簡単にはわからないことです。
この問題に対する 1 つの解決策は、ユニット・テストを自動化し、ブラウザーが異なってもコードが正常に動作するかどうかを検証することです
JavaScript ユニット・テスト・フレームワークとして非常に優れているものの 1 つが JsTestDriver ライブラリーです。このライブラリーは多様なブラウザーに対して JavaScript コードのテストをすることができます。図 1 に JsTestDriver のアーキテクチャーを示します。
図 1. JsTestDriver のアーキテクチャー
サーバーはブラウザーからの接続を受け付けると、そのブラウザーに JavaScript テスト・ケースのランナー・コードをロードします。さまざまなブラウザーからの接続に対して、サーバーはこれを繰り返します。ブラウザーからサーバーへの接続はコマンドラインから行うことも、そのブラウザーでサーバーの URL を指定して行うこともできます。いったんブラウザーが接続されると、そのブラウザーはスレーブ・ブラウザーと呼ばれます。サーバーは JavaScript コードをロードし、すべてのブラウザーでテスト・ケースを実行し、その結果をクライアントに返します。
クライアント (コマンドライン) に必要とされる主なものには、以下の 2 つがあります。
- JavaScript ファイル — ソース・ファイルとテスト・ファイル
- 構成ファイル — ソース・ファイルとテスト・ファイルのロードを調整するためのファイル
このアーキテクチャーは柔軟であり、1 つのサーバーが、ネットワーク上に存在するマシンのさまざまなブラウザーからの接続をいくらでも受け付けることができます。このアーキテクチャーが有用である例として、あるコードが Linux 上で実行されており、テスト・ケースを別の Windows マシンの Microsoft Internet Explorer に対して実行したい、といった場合があります。
JsTestDriver ライブラリーを使用するには、まず最新バージョンの JsTestDriver 1.3.2 をダウンロードしてください。
では、JavaScript のテスト・ケースを作成しましょう。単純にするために、ここでは以下のケースをテストします。
- ユーザー名の値が空で、パスワードの値が空の場合
- ユーザー名の値が空で、パスワードの値が空ではない場合
- ユーザー名の値が空ではなく、パスワードの値が空の場合
リスト 3 に、TestCase オブジェクトを表現する ApplicationUtilTest オブジェクトのコードの一部を示します。
リスト 3. ApplicationUtilTest オブジェクトのコードの一部
ApplicationUtilTest = TestCase("ApplicationUtilTest");
ApplicationUtilTest.prototype.setUp = function () {
/*:DOC += <FORM action=""><table><tr><td>Username</td><td>
<input type="text" id="username"/></td><td><span id="usernameMessage">
</span></td></tr><tr><td>Password</td><td>
<input type="password" id="password"/></td><td><span id="passwordMessage"
></span></td></tr></table></FORM>*/
};
ApplicationUtilTest.prototype.testValidateLoginFormBothEmpty = function () {
var applicationUtil = new appnamespace.ApplicationUtil();
/* Simulate empty user name and password */
document.getElementById("username").value = "";
document.getElementById("password").value = "";
applicationUtil.validateLoginForm();
assertEquals("Username is not validated correctly!", "This field is required",
document.getElementById("usernameMessage").innerHTML);
assertEquals("Password is not validated correctly!", "This field is required",
document.getElementById("passwordMessage").innerHTML);
};
|
ApplicationUtilTest オブジェクトは JsTestDriver の TestCase オブジェクトを使用して作成されます。JUnit フレームワークを使い慣れている人であれば、setUp メソッドや testXXX メソッドはおなじみのはずです。setUp
メソッドはテスト・ケースの初期化に使用されます。この例では、他のテスト・ケース・メソッドに使用する HTML フラグメントを setUp メソッドを使用して宣言しています。
DOC アノテーションは、HTML フラグメントを宣言するために簡単に使える JsTestDriver の規約です。
testValidateLoginFormBothEmpty メソッドの中で、テスト・ケース・メソッド内で使用する
ApplicationUtil オブジェクトを作成しています。このコードは次に、ユーザー名とパスワードの DOM
要素を取得して値を空に設定することで、ユーザー名とパスワードの入力が空の場合をシミュレートしています。validateLoginForm メソッドを呼び出すことで実際のフォームの検証が行われています。最後に、assertEquals が呼び出され、span 要素 usernameMessage に含まれるメッセージと span 要素 passwordMessage に含まれるメッセージが適切であるかどうか (適切なメッセージは「This field is required」です) を確認しています。
JsTestDriver では、以下の構成体を使用することができます。
fail("msg")— テストを失敗させる必要があり、メッセージ・パラメーターがエラー・メッセージとして表示されることを示します。assertTrue("msg", actual)— actual パラメーターが真であることをアサートします。真でない場合には、メッセージ・パラメーターがエラー・メッセージとして表示されます。assertFalse("msg", actual)— actual パラメーターが偽であることをアサートします。偽でない場合には、メッセージ・パラメーターがエラー・メッセージとして表示されます。assertSame("msg", expected, actual)— actual パラメーターが expected パラメーターと同じであることをアサートします。同じではない場合には、メッセージ・パラメーターがエラー・メッセージとして表示されます。assertNotSame("msg", expected, actual)— actual パラメーターが expected パラメーターと同じではないことをアサートします。同じ場合には、メッセージ・パラメーターがエラー・メッセージとして表示されます。assertNull("msg", actual)— actual パラメーターがヌルであることをアサートします。ヌルでない場合には、メッセージ・パラメーターがエラー・メッセージとして表示されます。assertNotNull("msg", actual)— actual パラメーターがヌルではないことをアサートします。ヌルである場合には、メッセージ・パラメーターがエラー・メッセージとして表示されます。
他のメソッドのコードには他のテスト・ケースが含まれています。リスト 4 はテスト・ケース・オブジェクトの完全なコードを示しています。
リスト 4. ApplicationUtil オブジェクトの完全なコード
ApplicationUtilTest = TestCase("ApplicationUtilTest");
ApplicationUtilTest.prototype.setUp = function () {
/*:DOC += <FORM action=""><table><tr><td>Username</td><td>
<input type="text" id="username"/></td><td><span id="usernameMessage">
</span></td></tr><tr><td>Password</td><td>
<input type="password" id="password"/></td><td><span id="passwordMessage"
></span></td></tr></table></FORM>*/
};
ApplicationUtilTest.prototype.testValidateLoginFormBothEmpty = function () {
var applicationUtil = new appnamespace.ApplicationUtil();
/* Simulate empty user name and password */
document.getElementById("username").value = "";
document.getElementById("password").value = "";
applicationUtil.validateLoginForm();
assertEquals("Username is not validated correctly!", "This field is required",
document.getElementById("usernameMessage").innerHTML);
assertEquals("Password is not validated correctly!", "This field is required",
document.getElementById("passwordMessage").innerHTML);
};
ApplicationUtilTest.prototype.testValidateLoginFormWithEmptyUserName = function () {
var applicationUtil = new appnamespace.ApplicationUtil();
/* Simulate empty user name and password */
document.getElementById("username").value = "";
document.getElementById("password").value = "anyPassword";
applicationUtil.validateLoginForm();
assertEquals("Username is not validated correctly!",
"This field is required", document.getElementById("usernameMessage").innerHTML);
assertEquals("Password is not validated correctly!",
"", document.getElementById("passwordMessage").innerHTML);
};
ApplicationUtilTest.prototype.testValidateLoginFormWithEmptyPassword = function () {
var applicationUtil = new appnamespace.ApplicationUtil();
document.getElementById("username").value = "anyUserName";
document.getElementById("password").value = "";
applicationUtil.validateLoginForm();
assertEquals("Username is not validated correctly!",
"", document.getElementById("usernameMessage").innerHTML);
assertEquals("Password is not validated correctly!",
"This field is required", document.getElementById("passwordMessage").
innerHTML);
};
|
JavaScript コードをテストするための推奨プラクティスの 1 つは、JavaScript ソース・コードを JavaScript テスト・コードとは別のフォルダーに配置することです。図 2 の例では、JavaScript ソースのフォルダーを「js-src」という名前に、JavaScript テストのフォルダーを「js-test」という名前にし、「js」という親フォルダーの下に配置しました。
図 2. JavaScript テスト・フォルダーの構造
ソース・フォルダーとテスト・フォルダーの両方の構造を決めたら、構成ファイルを作成する必要があります。デフォルトで、JsTestDriver ランナーは jsTestDriver.conf
という名前の構成ファイルを探します。構成ファイルの名前はコマンドラインから変更することができます。リスト 5 は JsTestDriver 構成ファイルの内容を示しています。
リスト 5. JsTestDriver 構成ファイルの内容
server: http://localhost:9876 load: - js-src/*.js - js-test/*.js |
この構成ファイルは YAML フォーマットで作成されています。server
ディレクティブはテスト・サーバーのアドレスを指定し、一方 load ディレクティブは、どの JavaScript ファイルをどういう順序でブラウザーにロードするかを示しています。
ここで、IE、Firefox、Safari ブラウザーでテスト・ケース・クラスを実行してみましょう。
テスト・ケース・クラスを実行するためにはサーバーを起動する必要があります。以下のコマンドラインを実行すると、JsTestDriver サーバーを起動することができます。
java -jar JsTestDriver-1.3.2.jar --port 9876 --browser "[Firefox Path]",
"[IE Path]","[Safari Path]"
|
このコマンドラインを使用すると、サーバーはポート 9876 で起動され、マシン上にある Firefox、IE、Safari ブラウザーからの接続を受け付けます。
ブラウザーを起動し、ブラウザーからサーバーへ接続すると、以下のコマンドラインを実行することでテスト・ケース・クラスを実行することができます。
java -jar JsTestDriver-1.3.2.jar --tests all |
上記のコマンドを実行すると、最初の実行結果が表示されます (リスト 6)。
リスト 6. 最初の実行結果
Total 9 tests (Passed: 6; Fails: 3; Errors: 0) (16.00 ms)
Firefox 3.6.18 Windows: Run 3 tests (Passed: 0; Fails: 3; Errors 0) (8.00 ms)
ApplicationUtilTest.testValidateLoginFormBothEmpty failed (3.00 ms):
AssertError: Username is not validated correctly! expected "This field
is required" but was "" Error("Username is not validated correctly!
expected \"This field is required\" but was \"\"")@:0()@http://localhost
:9876/test/js-test/TestApplicationUtil.js:16
ApplicationUtilTest.testValidateLoginFormWithEmptyUserName failed (3.00 ms):
AssertError: Username is not validated correctly! expected "This field is
required" but was "" Error("Username is not validated correctly! expected
\"This field is required\" but was \"\"")@:0()@http://localhost:9876/test
/js-test/TestApplicationUtil.js:29
ApplicationUtilTest.testValidateLoginFormWithEmptyPassword failed (2.00 ms):
AssertError: Password is not validated correctly! expected "This field is
required" but was "" Error("Password is not validated correctly! expected
\"This field is required\" but was \"\"")@:0()@http://localhost:9876/test/
js-test/TestApplicationUtil.js:42
Safari 534.50 Windows: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (2.00 ms)
Microsoft Internet Explorer 8.0 Windows: Run 3 tests (Passed: 3; Fails: 0;
Errors 0) (16.00 ms)
Tests failed: Tests failed. See log for details.
|
リスト 6 を見ると、Firefox の場合に主な問題があることがわかります。このテストでは、Internet Explorer の場合も Safari の場合も、テスト対象のコードは正常に実行されました。
JavaScript コードを修正し、テスト・ケースを再度実行する
では、問題のある JavaScript コードを修正しましょう。ここでは、innerText の代わりに innerHTML を使用することにします。リスト 7 は修正した ApplicationUtil オブジェクトのコードを示しています。
リスト 7. 修正した ApplicationUtil オブジェクトのコード
appnamespace = {};
appnamespace.ApplicationUtil = function() {};
appnamespace.ApplicationUtil.prototype.validateLoginForm = function(){
var error = true;
document.getElementById("usernameMessage").innerHTML = "";
document.getElementById("passwordMessage").innerHTML = "";
if (!document.getElementById("username").value) {
document.getElementById("usernameMessage").innerHTML =
"This field is required";
error = false;
}
if (!document.getElementById("password").value) {
document.getElementById("passwordMessage").innerHTML =
"This field is required";
error = false;
}
return error;
};
|
--test all コマンドライン引数を使用して、このテスト・ケース・オブジェクトを再度実行します。リスト 8 は 2 回目の実行結果を示しています。
リスト 8. 2 回目の実行結果
Total 9 tests (Passed: 9; Fails: 0; Errors: 0) (9.00 ms) Firefox 3.6.18 Windows: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (9.00 ms) Safari 534.50 Windows: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (5.00 ms) Microsoft Internet Explorer 8.0 Windows: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (0.00 ms) |
リスト 8 を見るとわかるように、今度はこの JavaScript コードは IE、Firefox、Safari で問題なく動作します。
この記事では、JavaScript のユニット・テストのための最も強力なツールの 1 つ (JsTestDriver) を使用して、さまざまなブラウザーに対して JavaScript のアプリケーション・コードをテストする方法を学びました。さらに、JsTestDriver の概要、JsTestDriver の構成方法、そして Web アプリケーションの中で JsTestDriver を使用して JavaScript コードの品質と堅牢さを確実にする方法についても学習しました。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| The source code | simple.zip | 3.35MB | HTTP |
学ぶために
- JUnit.org を訪れ、JUnit テスト・フレームワークの使い方を学んでください。
- YAML について学んでください。YAML は人間にも理解しやすいデータ・シリアライズ標準であり、すべてのプログラミング言語を対象としています。
- developerWorks の Open source ゾーンには、オープンソース技術を使用した開発や、IBM 製品でオープンソース技術を使用するためのハウ・ツー情報やツール、プロジェクトの更新情報など、豊富な情報が用意されています。
- developerWorks の Technical events and
webcasts に参加して最新情報を入手してください。
- Twitter で developerWorks のツイートをフォローしてください。
- developerWorks podcasts でソフトウェア開発に関する興味深いインタビューや議論を聞いてください。
製品や技術を入手するために
- 皆さんはアプリケーションのテストに関心を持っているので、IBM Rational
Functional Tester の試用版を試してみてください。
- JsTestDriver
のダウンロード・ページから最新のコードやプラグインその他を入手してください。
- ユニット・テストのための別のツールとして、Dojo プロジェクトの Web サイトから入手できる Dojo Objective Harness (DOH) を試してみてください。
- JavaScript
のためのさまざまなユニット・テスト・フレームワークについて調べてください。
議論するために
- 開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、developerWorks コミュニティーを通じて他の developerWorks ユーザーとやり取りしてください。
- developerWorks のコミュニティー、Real
world open source グループの構築を支援してください。

Hazem Saleh は Java Enterprise とオープンソース技術に 7 年の経験があります。彼は Apache のコミッターであり、『The Definitive Guide to Apache MyFaces and Facelets』(Apress 刊) の著者でもあります。また彼は多くの技術記事も執筆しており、developerworks への寄稿者であり、国内外のカンファレンスで技術講演も行っています。彼は JavaOne と IBM Regional Technical Exchange で講演したこともあります。彼は IBM Egypt の Web 2.0 テクニカル・エキスパートであり、スタッフ・ソフトウェア・エンジニアでもあります。