JavaScript のユニット・テストを効率よく行う

さまざまなブラウザーに対する JavaScript コードのテストを自動化する

あるブラウザーで正常に実行できる JavaScript コードが必ずしも他のブラウザーで正常に動作するとは限りません。JavaScript コードに対してユニット・テストを実行しない場合には、新しいブラウザーへのアップグレードや新しいブラウザーのサポートを決定する際、組織は Web アプリケーションのテストや再テストに費用をかけることになります。この記事では、JavaScript のユニット・テストを効率的に行うことにより、いかにテスト・コストを削減することができ、多くのブラウザーを容易にサポートできるようになるかを学びます。

Hazem Saleh, Staff Software Engineer, IBM  

Photo of Hazem SalehHazem 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 テクニカル・エキスパートであり、スタッフ・ソフトウェア・エンジニアでもあります。



2011年 10月 28日

問題のある JavaScript コードの例

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 つの解決策は、ユニット・テストを自動化し、ブラウザーが異なってもコードが正常に動作するかどうかを検証することです


JsTestDriver

JavaScript ユニット・テスト・フレームワークとして非常に優れているものの 1 つが JsTestDriver ライブラリーです。このライブラリーは多様なブラウザーに対して JavaScript コードのテストをすることができます。図 1 に JsTestDriver のアーキテクチャーを示します。

図 1. JsTestDriver のアーキテクチャー
JsTestDriver のアーキテクチャー

jsTestDriver はオープンソースです

jsTestDriver は Apache 2.0 の下で行われているオープンソース・プロジェクトであり、SourceForge に似たプロジェクト・リポジトリーである Google Code でホストされています。開発者は、Open Source Initiative で承認されたライセンスの 1 つを使用する限り、このリポジトリーで公開プロジェクトを作成、管理することができます。

他にもたくさんの JavaScript ユニット・テスト・ツールがあります。Dojo Objective Harness (DOH) など、他のツールについては記事の終りのほうにある「参考文献」セクションを参照してください。

サーバーはブラウザーからの接続を受け付けると、そのブラウザーに JavaScript テスト・ケースのランナー・コードをロードします。さまざまなブラウザーからの接続に対して、サーバーはこれを繰り返します。ブラウザーからサーバーへの接続はコマンドラインから行うことも、そのブラウザーでサーバーの URL を指定して行うこともできます。いったんブラウザーが接続されると、そのブラウザーはスレーブ・ブラウザーと呼ばれます。サーバーは JavaScript コードをロードし、すべてのブラウザーでテスト・ケースを実行し、その結果をクライアントに返します。

クライアント (コマンドライン) に必要とされる主なものには、以下の 2 つがあります。

  1. JavaScript ファイル — ソース・ファイルとテスト・ファイル
  2. 構成ファイル — ソース・ファイルとテスト・ファイルのロードを調整するためのファイル

このアーキテクチャーは柔軟であり、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 テスト・フォルダーの構造
JavaScript テスト・フォルダーの構造

ソース・フォルダーとテスト・フォルダーの両方の構造を決めたら、構成ファイルを作成する必要があります。デフォルトで、JsTestDriver ランナーは jsTestDriver.conf という名前の構成ファイルを探します。構成ファイルの名前はコマンドラインから変更することができます。リスト 5JsTestDriver 構成ファイルの内容を示しています。

リスト 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 codesimple.zip3.35MB

参考文献

学ぶために

  • JUnit.org を訪れ、JUnit テスト・フレームワークの使い方を学んでください。
  • YAML について学んでください。YAML は人間にも理解しやすいデータ・シリアライズ標準であり、すべてのプログラミング言語を対象としています。
  • developerWorks の Open source ゾーンには、オープンソース技術を使用した開発や、IBM 製品でオープンソース技術を使用するためのハウ・ツー情報やツール、プロジェクトの更新情報など、豊富な情報が用意されています。
  • developerWorks の Technical events and webcasts に参加して最新情報を入手してください。
  • Twitter で developerWorks のツイートをフォローしてください。
  • developerWorks podcasts でソフトウェア開発に関する興味深いインタビューや議論を聞いてください。

製品や技術を入手するために

議論するために

  • 開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、developerWorks コミュニティーを通じて他の developerWorks ユーザーとやり取りしてください。
  • developerWorks のコミュニティー、Real world open source グループの構築を支援してください。

コメント

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=Open source, Java technology
ArticleID=767326
ArticleTitle=JavaScript のユニット・テストを効率よく行う
publish-date=10282011