2011年11月に中途で入社し、現在はAmeba事業本部でスマートフォン版Ameba(通称デカグラフ)の開発をしている川口(facebook)と申します。
スマートフォン版Amebaではサーバー部分のNode.jsとフロント部分のJavaScriptを担当しています。趣味はWEBサービス制作で、最近の楽しみはぁゃぴ日報を読むことです。(社内ネタですすいません)
JavaScriptにおけるテストフレームワーク
●JsUnit
Javaのテストフレームワークとして有名な「JUnit」を参考に作られたテストフレームワークです。
●QUnit
もともとjQueryをテストするために開発されたフレームワークですが、現在ではjQueryへの依存が無くなっているためjQuery以外のライブラリを使ったプロジェクトでも使用できます。
●Jasmine
Jasmineを用いたテスト(準備編)
今現在の流れで言うと、全体としてはBDDの機能をもったテストフレームワークに人気が集まっていると感じています。そのため、今回はJasmineを用いてフロントエンドのテストコードを書いていきます。
●使用するライブラリ
・jQuery(1.7.2)
・Jasmine(1.2.0)
●テストを実行するサンプル
JSのテスト手法を解説したブログなどを見ていつも思っていたことなのですが、細かい関数単位でのテストの書き方は解説していても、全体の動きを通したテストの書き方を解説しているところは少ないと感じていました。(そもそもそういうテストはSeleniumを使えと言われるかもしれませんが・・・)
そのため今回は全体の動きを通したテストとして、モーダルウィンドウを表示するサンプルをテスト対象として使用していきます。
HTML
<div id="root"></div>
JavaScript(modal.js)
var exports = {};
var event = {
documentReady: function() {
$('#root').append(view.main);
// ボタンのイベントをセット
view.button.click(event.showModalWindow);
view.main.append(view.button);
},
showModalWindow: function() {
model.getDetail(function(detail) {
view.modalWindow.find('.title').append(detail.title);
view.modalWindow.find('.description').append(detail.description);
view.main.append(view.modalWindow);
});
}
};
var model = {
// 詳細情報の取得用関数
getDetail: function(callback) {
$.get('./detail.json', function(data) {
callback(JSON.parse(data));
});
}
};
var view = {
main: $('<div id="main"></div>'),
button: $('<button>Show Modal Window</button>'),
modalWindow: $('<div class="modal"><p class="title"></p><p class="description"></p></div>')
};
$(document).ready(event.documentReady);
// テスト用
exports.event = event;
exports.model = model;
exports.view = view;
return exports;
})(window);
サンプルの動きとしては、
1.ボタンをクリック
2.モーダルウィンドウが表示され、そこに商品などに関する詳細情報が表示される
というよくある流れです。
Jasmineを用いたテスト(実行編)
●テストを実行するための関数の用意ここで、テストを実行するためのユーティリティ関数を用意していきます。
関数の内容としては以下の通りです。
・$.test.require
テストの対象となるJSファイルを動的に読み込むための関数です。
スクリプトタグを追加する形を取らず、$.getで取得したJSの文字列をevalで実行すること
によって即時関数の返却値を取得し、テストを実行するためだけにグローバルな関数を
定義することを減らすためのものです。
・$.test.exec
Jasmineのテストを実行するための関数です。
Javascript(test.js)
(function(w) {
var slice = Array.prototype.slice;
$.test = $.test || {};
function isObject(o) {
return o instanceof Object;
}
// テスト対象のJSを動的に読み込むための関数
$.test.require = function() {
var args = slice.apply(arguments);
var callback = args.pop();
var len = args.length;
var i = 0;
var scripts = {};
var exec = function() {
var url = args[i];
var beforeEval = null;
var name = null;
if (isObject(url)) {
beforeEval = url.beforeEval;
name = url.name;
url = url.path;
}
$.get(url, function(script) {
if (beforeEval) {
script = beforeEval(script);
}
var resEval = eval(script);
scripts[name || url] = resEval;
i++;
if (i === len && callback) {
callback(scripts);
} else {
exec();
}
});
};
exec();
};
// テストの実行
$.test.exec = function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
jasmineEnv.execute();
};
})(window);
●テストコード
全ての準備が整いましたので、ここでやっとテストコードを書いていきます!
テストの流れとしては、
1.ボタンをクリック
2.表示されたモーダルウィンドウの状態をチェック
という風になっています。
JavaScript(spec/modal.js)
(function() {
var modal = null;
var event = null;
var model = null;
var views = null;
$.test.require(
// modal.jsの読み込み
{name:'modal', path:'../js/modal.js', beforeEval:function(script) {
// veiw.mainの#rootへのappendが実行されないようコードを削除
return script.replace("$('#root').append(view.main);", '');
}
},
function(module) {
modal = module.modal;
// modal.jsでexportsしておいたオブジェクトをここで取得できる
event = modal.event;
model = modal.model
view = modal.view;
// テストの実行
$.test.exec();
}
);
var base = null;
describe('モーダルウィンドウテスト', function() {
var temp = null;
var cnt = 0;
beforeEach(function() {
// 1度だけ実行されるようにする
if (cnt > 0) { return; }
// 詳細データ取得用の関数を一時的に書き換え
temp = model.getDetail;
model.getDetail = function(callback) {
callback({title:'テスト・タイトル', description:'テスト・説明'});
};
// ボタンのクリック
view.button.click();
model.getDetail = temp;
cnt++;
});
describe('表示チェック', function() {
it('ウィンドウ', function() {
expect(view.main.find('.modal').length === 1).toBe(true);
});
it('タイトル', function() {
var title = view.modalWindow.find('.title').text()
expect(title === 'テスト・タイトル').toBe(true);
});
it('説明', function() {
var description = view.modalWindow.find('.description').text();
expect(description === 'テスト・説明').toBe(true);
});
});});
})();
テストの結果はどうでしたでしょうか?
無事成功していれば以下のような画面が表示されると思います。
今回のサンプルをGithub上に上げていますので、よければそちらもご覧ください。
github|1pixelTestSample
おわりに
コードが多めの記事になってしまったため見づらかったかもしれませんが、最後までお付き合いいただきありがとうございました。これを機にJavascriptでのテストに興味を持っていただければ幸いです。
実際にこのテスト手法を取り入れてスマートフォン版Amebaをリニューアルしていますので、よければご覧ください。
スマートフォン版Ameba