javascriptのテストのはなし:YUI

今回はYUIのユニットテストを紹介します。
YUIは「Yahoo! User Interface Library」の略称で、webアプリケーション開発用のJavaScriptとCSSのユーティリティ群です。
http://developer.yahoo.co.jp/yui/によると

YUIは拡張性、速さ、堅牢さに実績があり、米国Yahoo!や世界中のフロントエンドエンジニアによって開発されているため、JavaScriptを活用するプロフェッショナルの業務にも耐えうる実用性を持ったJavaScriptライブラリとなっています。

との事です。

準備

現時点でのYUIの最新版は3.4.1でhttp://yui.yahooapis.com/3.4.1/build/yui/yui-min.jsにホスティングされているので、それを読み込んでおきます。

記述

基本

YUI.use("test", function(Y){
	//テストを記述
});

としてテストを記述していきます。YUI.use()で使うモジュールをロードして、そのモジュールをYとして扱うというイメージですね。
テストを実行する場合、Console.logが対応しているブラウザであれば、デバッガのコンソールで結果を確認できます。

テストケースの記述

テストケースはY.Test.Caseクラスを使って作ります。インスタンスを作成するときに引数に、name、テストケースのメソッド(、setUp、tearDown)を含んだオブジェクトを渡します。こんな感じです。

var testCase = new Y.Test.Case({
	name : "testcase name",
	setUp : function(){},
	tearDown : function(){},
	testSomething : function(){},
	testSomethingElse : function(){},
	"Something should happen here" : function(){}
});

テストケースのメソッドは"test"から始まる名前を付けるか、"Something should happen here"の様に"should"を含んだ文章調にする事も可能です。

ignore、error

テストケース内に_shouldの要素を作る事で、ignoreとerrorの機能が使えます。
ignoreは特定のテストを実施しない様にします。メソッド名とBooleanのセットにしたオブジェクトです。
errorはテスト対象のロジックでエラーが発生した場合のテストになります。trueとすればErrorが発生したかどうかだけ、"error occured"などの文字列にすればエラー内容、new TypeError("error occured")とすれば型までチェックしてくれます。

var testCase = new Y.Test.Case({
	name : "testcase name",
	_should:{
		ignore : {
			testSomethingElse:true
		}
		error : {
			testSomething:true
			//testSomething : "error occured"
			//testSomething : new TypeError("error occured")
		}
	}
	setUp : function(){...},
	tearDown : function(){...},
	testSomething : function(){...},
	"Something should happen here" : function(){...}
});

テストスイート作成とテスト実行

テストスイートの作成とケースの追加、テスト実行は次の様に行います。

var suite = new Y.Test.Suite({
	name: "test suite",
});
suite.add(testCase);

Y.Test.Runner.add(suite);
Y.Test.Runner.run();

非同期テスト

wait(milliseconds)メソッドを使う事で、テスト実行を指定したミリ秒待つ事ができます。

var testCase = new Y.Test.Case({
	name : "async test",
	testSomething : function(){
		this.wait(function(){...}, 1000);
	}
});

モック

モックオブジェクトを作る事で、サービスアクセス時のパラメタのテストが出来ます。流れは、

  1. Y.Mock()でMockオブジェクト作成
  2. Y.Mock.expect()で期待する動作の設定
  3. 対象の処理実行
  4. Y.Mock.verify()で検証

です。
Y.Mock.expect()は、第1引数にはMockオブジェクトを、第2引数にはメソッド名と引数がセットになったオブジェクトを取ります。
次の様な感じになります。

var testCase = new Y.Test.Case({
	name: "logToServer Tests",
	
	testPassingDataToXhr: function(){
		var mockXhr = Y.Mock();
		
		Y.Mock.expect(mockXhr, {
			method: "open", 
			args: ["get", "/log.php?msg=%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A", true]
		});
		
		Y.Mock.expect(mockXhr, {
			method: "send", 
			args: [null]
		});
		
		logToServer("こんにちは", mockXhr);
		
		Y.Mock.verify(mockXhr);
	}
});

function logToServer(message, xhr){
	xhr.open("get", "/log.php?msg=" + encodeURIComponent(message), true);
	xhr.send(null);
}

実行する

このブログの最後に掲載したテストコードを実行した結果は次の様になります。Firebugのコンソール画面ですが、色分けされないのでみにくいですね。

Consoleを使ってみる

Y.Concoleというテスト結果を出力する機能があるので使ってみます。まずはYUI().use("test", function(Y){...})としているところを、YUI().use("test", "console", function(Y){...})としてconsoleを使う事を宣言します。
また、次のコードをY.Test.Runner.run()する前に記述し、コンソールのインスタンスを作って表示させます。"#testLogger"はdiv要素です。

var r = new Y.Console({
	newestOnTop : false,
	style: "block"
});
r.render("#testLogger");

最後にコンソールを装飾しておきます。このままだと何も装飾されていなく見にくいので次のコードをhtmlに記述しておきます。

<div class="example yui3-skin-sam">
<style scoped>
#testLogger {
	margin-bottom: 1em;
}
#testLogger .yui3-console .yui3-console-title {
	border: 0 none;
	color: #000;
	font-size: 13px;
	font-weight: bold;
margin: 0;
	text-transform: none;
}
#testLogger .yui3-console .yui3-console-entry-meta {
	margin: 0;
}
.yui3-skin-sam .yui3-console-entry-pass .yui3-console-entry-cat {
	background: #070;
	color: #fff;
}
</style>

<div id="testLogger"></div>
</div>

これで実行すると次の様な表示になり、ブラウザのコンソールで見るよりはまだ見やすいかもしれません。

実行したコード

<html lang="en">
<head>
	<title>YUI Test</title>
	<script src="http://yui.yahooapis.com/3.4.1/build/yui/yui-min.js"></script>
</head>

<body>
<script type="text/javascript">
	YUI().use("test", function(Y){
		var testCase = new Y.Test.Case({
			name: "Simple Test Case",
			setUp: function(){
				this.hoge = {name:"case", val:"test"};
			},
			tearDown: function(){
				delete this.val;
			},
			
			_should:{
				ignore:{
					testSomethingElse: true
				},
				error:{
					testSortArray: new TypeError("Expected an array")
				}
			},
			
			testSomething: function(){
				Y.assert(this.hoge.name == "case");
				Y.Assert.areSame(this.hoge.val, "test");
				Y.Assert.isObject(this.hoge);
				Y.Assert.isTypeOf("object", this.hoge);
				Y.Assert.isInstanceOf(Object, this.hoge);
				Y.Assert.isNaN("NaN");
				this.hoge.val = "test2"
			},
			
			testSomethingElse: function(){
				Y.assert(true, "third test");
			},
			"somethig should happen": function(){
				Y.Assert.areNotSame("test2", this.hoge.val);
			},
			testSortArray: function(){
				sortArray(12);
			}
		});
		
		var asyncTestCase = new Y.Test.Case({
			name: "logToServer Tests",
			
			testPassingDataToXhr: function(){
				var mockXhr = Y.Mock();
				
				Y.Mock.expect(mockXhr, {
					method: "open", 
					args: ["get", "/log.php?msg=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF", true]
				});
				
				Y.Mock.expect(mockXhr, {
					method: "send", 
					args: [null]
				});
				
				logToServer("こんにちは", mockXhr);
				
				Y.Mock.verify(mockXhr);
			}
		});
		
		
		var mocTestCase = new Y.Test.Case({
			name: "Async Test",
			
			setUp : function () {
				this.data = { name : "Nicholas", age : 29 };
			},
			
			tearDown : function () {
				delete this.data;
			},
			
			testAsync: function () {
				Y.Assert.areEqual("Nicholas", this.data.name, "Name should be 'Nicholas'");
				
				//wait 1000 milliseconds and then run this function
				this.wait(function(){
					Y.Assert.areEqual(29, this.data.age, "Age should be 29");
				}, 1000);
			}
		});
		
		var suite = new Y.Test.Suite({
			name: "test suite",
		});
		
		suite.add(testCase);
		suite.add(mocTestCase);
		suite.add(asyncTestCase);
		Y.Test.Runner.add(suite);
		Y.Test.Runner.run();
	});
	
	function logToServer(message, xhr){
		xhr.open("get", "/log.php?msg=" + encodeURIComponent(message), true);
		xhr.send(null);
	}
	
	function sortArray(array) {
		if (array instanceof Array){
			array.sort();
		} else {
			throw new TypeError("Expected an array");
		}
	}
</script>
</body>
</html>